senkyoshi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +92 -0
- data/bin/console +14 -0
- data/bin/import_blackboard +6 -0
- data/lib/senkyoshi/canvas_course.rb +179 -0
- data/lib/senkyoshi/collection.rb +26 -0
- data/lib/senkyoshi/config.rb +35 -0
- data/lib/senkyoshi/exceptions.rb +13 -0
- data/lib/senkyoshi/models/announcement.rb +36 -0
- data/lib/senkyoshi/models/answer.rb +32 -0
- data/lib/senkyoshi/models/assessment.rb +95 -0
- data/lib/senkyoshi/models/assignment.rb +26 -0
- data/lib/senkyoshi/models/assignment_group.rb +23 -0
- data/lib/senkyoshi/models/blog.rb +22 -0
- data/lib/senkyoshi/models/content.rb +92 -0
- data/lib/senkyoshi/models/content_file.rb +26 -0
- data/lib/senkyoshi/models/course.rb +39 -0
- data/lib/senkyoshi/models/file.rb +99 -0
- data/lib/senkyoshi/models/forum.rb +28 -0
- data/lib/senkyoshi/models/gradebook.rb +30 -0
- data/lib/senkyoshi/models/group.rb +22 -0
- data/lib/senkyoshi/models/module.rb +22 -0
- data/lib/senkyoshi/models/module_item.rb +23 -0
- data/lib/senkyoshi/models/question.rb +188 -0
- data/lib/senkyoshi/models/questions/calculated.rb +72 -0
- data/lib/senkyoshi/models/questions/either_or.rb +31 -0
- data/lib/senkyoshi/models/questions/essay.rb +11 -0
- data/lib/senkyoshi/models/questions/file_upload.rb +4 -0
- data/lib/senkyoshi/models/questions/fill_in_blank.rb +15 -0
- data/lib/senkyoshi/models/questions/fill_in_blank_plus.rb +20 -0
- data/lib/senkyoshi/models/questions/hot_spot.rb +11 -0
- data/lib/senkyoshi/models/questions/jumbled_sentence.rb +37 -0
- data/lib/senkyoshi/models/questions/matching.rb +57 -0
- data/lib/senkyoshi/models/questions/multiple_answer.rb +56 -0
- data/lib/senkyoshi/models/questions/multiple_choice.rb +18 -0
- data/lib/senkyoshi/models/questions/numerical.rb +31 -0
- data/lib/senkyoshi/models/questions/opinion_scale.rb +18 -0
- data/lib/senkyoshi/models/questions/ordering.rb +40 -0
- data/lib/senkyoshi/models/questions/quiz_bowl.rb +11 -0
- data/lib/senkyoshi/models/questions/short_response.rb +4 -0
- data/lib/senkyoshi/models/questions/true_false.rb +15 -0
- data/lib/senkyoshi/models/quiz.rb +7 -0
- data/lib/senkyoshi/models/resource.rb +33 -0
- data/lib/senkyoshi/models/scorm_package.rb +109 -0
- data/lib/senkyoshi/models/staff_info.rb +72 -0
- data/lib/senkyoshi/models/wikipage.rb +34 -0
- data/lib/senkyoshi/tasks.rb +90 -0
- data/lib/senkyoshi/version.rb +3 -0
- data/lib/senkyoshi/xml_parser.rb +154 -0
- data/lib/senkyoshi.rb +69 -0
- metadata +250 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class Content < Resource
|
5
|
+
CONTENT_TYPES = {
|
6
|
+
"x-bb-asmt-test-link" => "Quiz",
|
7
|
+
"x-bb-asmt-survey-link" => "Quiz",
|
8
|
+
"x-bb-assignment" => "Assignment",
|
9
|
+
"x-bbpi-selfpeer-type1" => "Assignment",
|
10
|
+
"x-bb-document" => "WikiPage",
|
11
|
+
"x-bb-file" => "WikiPage",
|
12
|
+
"x-bb-audio" => "WikiPage",
|
13
|
+
"x-bb-image" => "WikiPage",
|
14
|
+
"x-bb-video" => "WikiPage",
|
15
|
+
"x-bb-externallink" => "WikiPage",
|
16
|
+
"x-bb-blankpage" => "WikiPage",
|
17
|
+
"x-bb-lesson" => "WikiPage",
|
18
|
+
"x-bb-folder" => "WikiPage",
|
19
|
+
"x-bb-module-page" => "WikiPage",
|
20
|
+
"x-bb-lesson-plan" => "WikiPage",
|
21
|
+
"x-bb-syllabus" => "WikiPage",
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
attr_accessor(:title, :body, :id, :files)
|
25
|
+
|
26
|
+
def self.from(xml, pre_data)
|
27
|
+
type = xml.xpath("/CONTENT/CONTENTHANDLER/@value").first.text
|
28
|
+
type.slice! "resource/"
|
29
|
+
if content_type = CONTENT_TYPES[type]
|
30
|
+
content_class = Senkyoshi.const_get content_type
|
31
|
+
content = content_class.new
|
32
|
+
content.iterate_xml(xml, pre_data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def iterate_xml(xml, pre_data)
|
37
|
+
@points = pre_data[:points] || 0
|
38
|
+
@title = xml.xpath("/CONTENT/TITLE/@value").first.text
|
39
|
+
@url = xml.at("URL")["value"]
|
40
|
+
@body = xml.xpath("/CONTENT/BODY/TEXT").first.text
|
41
|
+
@type = xml.xpath("/CONTENT/RENDERTYPE/@value").first.text
|
42
|
+
@parent_id = pre_data[:parent_id]
|
43
|
+
bb_type = xml.xpath("/CONTENT/CONTENTHANDLER/@value").first.text
|
44
|
+
bb_type.slice! "resource/"
|
45
|
+
@module_type = CONTENT_TYPES[bb_type]
|
46
|
+
@id = xml.xpath("/CONTENT/@id").first.text
|
47
|
+
if pre_data[:assignment_id] && !pre_data[:assignment_id].empty?
|
48
|
+
@id = pre_data[:assignment_id]
|
49
|
+
end
|
50
|
+
@module_item = set_module if @module_type
|
51
|
+
@files = xml.xpath("//FILES/FILE").map do |file|
|
52
|
+
ContentFile.new(file)
|
53
|
+
end
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_pre_data(xml, file_name)
|
58
|
+
id = xml.xpath("/CONTENT/@id").first.text
|
59
|
+
parent_id = xml.xpath("/CONTENT/PARENTID/@value").first.text
|
60
|
+
{
|
61
|
+
id: id,
|
62
|
+
parent_id: parent_id,
|
63
|
+
file_name: file_name,
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_module
|
68
|
+
@module_type = "Quizzes::Quiz" if @module_type == "Quiz"
|
69
|
+
module_item = ModuleItem.new(@title, @module_type, @id)
|
70
|
+
module_item.canvas_conversion
|
71
|
+
end
|
72
|
+
|
73
|
+
def canvas_conversion(course, _resources = nil)
|
74
|
+
course
|
75
|
+
end
|
76
|
+
|
77
|
+
def create_module(course)
|
78
|
+
course.canvas_modules ||= []
|
79
|
+
cc_module = course.canvas_modules.
|
80
|
+
detect { |a| a.identifier == @parent_id }
|
81
|
+
if cc_module
|
82
|
+
cc_module.module_items << @module_item
|
83
|
+
else
|
84
|
+
cc_module = Module.new(@title, @parent_id)
|
85
|
+
cc_module = cc_module.canvas_conversion
|
86
|
+
cc_module.module_items << @module_item
|
87
|
+
course.canvas_modules << cc_module
|
88
|
+
end
|
89
|
+
course
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class ContentFile < Resource
|
5
|
+
attr_accessor(:id, :name, :linkname)
|
6
|
+
|
7
|
+
def initialize(xml)
|
8
|
+
@id = xml.xpath("./@id").first.text
|
9
|
+
@name = xml.xpath("./NAME").first.text
|
10
|
+
@linkname = xml.xpath("./LINKNAME/@value").first.text
|
11
|
+
end
|
12
|
+
|
13
|
+
def canvas_conversion(*)
|
14
|
+
query = "?canvas_download=1&canvas_qs_wrap=1"
|
15
|
+
href = "$IMS_CC_FILEBASE$/#{IMPORTED_FILES_DIRNAME}/#{@linkname}#{query}"
|
16
|
+
%{
|
17
|
+
<a
|
18
|
+
class="instructure_scribd_file instructure_file_link"
|
19
|
+
title="#{@linkname}"
|
20
|
+
href="#{href}">
|
21
|
+
#{@linkname}
|
22
|
+
</a>
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class Course < Resource
|
5
|
+
##
|
6
|
+
# This class represents a reader for one zip file, and allows the usage of
|
7
|
+
# individual files within zip file
|
8
|
+
##
|
9
|
+
def initialize
|
10
|
+
@course_code = ""
|
11
|
+
@identifier = ""
|
12
|
+
@title = ""
|
13
|
+
@description = ""
|
14
|
+
@is_public = false
|
15
|
+
@start_at = ""
|
16
|
+
@conclude_at = ""
|
17
|
+
end
|
18
|
+
|
19
|
+
def iterate_xml(data, _)
|
20
|
+
@identifier = data["id"]
|
21
|
+
@title = Senkyoshi.get_attribute_value(data, "TITLE")
|
22
|
+
@description = Senkyoshi.get_description(data)
|
23
|
+
@is_public = Senkyoshi.get_attribute_value(data, "ISAVAILABLE")
|
24
|
+
@start_at = Senkyoshi.get_attribute_value(data, "COURSESTART")
|
25
|
+
@conclude_at = Senkyoshi.get_attribute_value(data, "COURSEEND")
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def canvas_conversion(course, _resources = nil)
|
30
|
+
course.identifier = @identifier
|
31
|
+
course.title = @title
|
32
|
+
course.description = @description
|
33
|
+
course.is_public = @is_public
|
34
|
+
course.start_at = @start_at
|
35
|
+
course.conclude_at = @conclude_at
|
36
|
+
course
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
require "senkyoshi/exceptions"
|
3
|
+
|
4
|
+
module Senkyoshi
|
5
|
+
class SenkyoshiFile < Resource
|
6
|
+
attr_accessor(:id, :location, :name)
|
7
|
+
@@dir = nil
|
8
|
+
|
9
|
+
FILE_BLACKLIST = [
|
10
|
+
"*.dat",
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
def initialize(zip_entry)
|
14
|
+
path = zip_entry.name
|
15
|
+
id = File.basename(path)
|
16
|
+
xid = id[/__(xid-[0-9]+_[0-9]+)/, 1]
|
17
|
+
name = id.gsub(/__xid-[0-9]+_[0-9]+/, "")
|
18
|
+
|
19
|
+
@location = extract_file(zip_entry) # Location of file on local filesystem
|
20
|
+
@name = name
|
21
|
+
@id = id
|
22
|
+
@xid = xid
|
23
|
+
end
|
24
|
+
|
25
|
+
def matches_xid?(xid)
|
26
|
+
@xid == xid
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_file(entry)
|
30
|
+
@@dir ||= Dir.mktmpdir
|
31
|
+
|
32
|
+
name = "#{@@dir}/#{entry.name}"
|
33
|
+
path = File.dirname(name)
|
34
|
+
FileUtils.mkdir_p path unless Dir.exist? path
|
35
|
+
entry.extract(name)
|
36
|
+
name
|
37
|
+
end
|
38
|
+
|
39
|
+
def canvas_conversion(course, _resources = nil)
|
40
|
+
file = CanvasCc::CanvasCC::Models::CanvasFile.new
|
41
|
+
file.identifier = @id
|
42
|
+
file.file_location = @location
|
43
|
+
file.file_path = "#{IMPORTED_FILES_DIRNAME}/#{@name}"
|
44
|
+
file.hidden = false
|
45
|
+
|
46
|
+
course.files << file
|
47
|
+
course
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Remove temporary files
|
52
|
+
##
|
53
|
+
def self.cleanup
|
54
|
+
FileUtils.rm_r @@dir unless @@dir.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Determine if a file is on the blacklist
|
59
|
+
##
|
60
|
+
def self.blacklisted?(file)
|
61
|
+
FILE_BLACKLIST.any? { |list_item| File.fnmatch?(list_item, file.name) }
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Determine whether or not a file is a metadata file or not
|
66
|
+
##
|
67
|
+
def self.metadata_file?(file_names, file)
|
68
|
+
if File.extname(file.name) == ".xml"
|
69
|
+
# Detect and skip metadata files.
|
70
|
+
concrete_file = File.join(
|
71
|
+
File.dirname(file.name),
|
72
|
+
File.basename(file.name, ".xml"),
|
73
|
+
)
|
74
|
+
file_names.include?(concrete_file)
|
75
|
+
else
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Determine whether or not a file is a part of a scorm package
|
82
|
+
##
|
83
|
+
def self.belongs_to_scorm_package?(package_paths, file)
|
84
|
+
package_paths.any? do |path|
|
85
|
+
File.dirname(file.name).start_with? path
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Determine if a file should be included in course files or not
|
91
|
+
##
|
92
|
+
def self.valid_file?(file_names, scorm_paths, file)
|
93
|
+
return false if SenkyoshiFile.blacklisted? file
|
94
|
+
return false if SenkyoshiFile.metadata_file? file_names, file
|
95
|
+
return false if SenkyoshiFile.belongs_to_scorm_package? scorm_paths, file
|
96
|
+
true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class Forum < Resource
|
5
|
+
def initialize
|
6
|
+
@title = ""
|
7
|
+
@text = ""
|
8
|
+
@identifier = Senkyoshi.create_random_hex
|
9
|
+
@discussion_type = "threaded"
|
10
|
+
end
|
11
|
+
|
12
|
+
def iterate_xml(data, _)
|
13
|
+
@title = Senkyoshi.get_attribute_value(data, "TITLE")
|
14
|
+
@text = Senkyoshi.get_text(data, "TEXT")
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def canvas_conversion(course, _resources = nil)
|
19
|
+
discussion = CanvasCc::CanvasCC::Models::Discussion.new
|
20
|
+
discussion.title = @title
|
21
|
+
discussion.text = @text
|
22
|
+
discussion.identifier = @identifier
|
23
|
+
discussion.discussion_type = @discussion_type
|
24
|
+
course.discussions << discussion
|
25
|
+
course
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Senkyoshi
|
2
|
+
class Gradebook
|
3
|
+
def get_pre_data(data, _)
|
4
|
+
categories = get_categories(data)
|
5
|
+
data.search("OUTCOMEDEFINITIONS").children.map do |outcome|
|
6
|
+
content_id = outcome.at("CONTENTID").attributes["value"].value
|
7
|
+
assignment_id = outcome.at("ASIDATAID").attributes["value"].value
|
8
|
+
category_id = outcome.at("CATEGORYID").attributes["value"].value
|
9
|
+
category = categories[category_id]
|
10
|
+
points = outcome.at("POINTSPOSSIBLE").attributes["value"].value
|
11
|
+
{
|
12
|
+
category: category,
|
13
|
+
points: points,
|
14
|
+
content_id: content_id,
|
15
|
+
assignment_id: assignment_id,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_categories(data)
|
21
|
+
data.at("CATEGORIES").children.
|
22
|
+
each_with_object({}) do |category, categories|
|
23
|
+
id = category.attributes["id"].value
|
24
|
+
title = category.at("TITLE").
|
25
|
+
attributes["value"].value.gsub(".name", "")
|
26
|
+
categories[id] = title
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class Group < Resource
|
5
|
+
def initialize
|
6
|
+
@name = ""
|
7
|
+
@description = ""
|
8
|
+
@is_public = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def iterate_xml(data, _)
|
12
|
+
@name = Senkyoshi.get_attribute_value(data, "TITLE")
|
13
|
+
@description = data.at("TEXT").children.text
|
14
|
+
@is_public = Senkyoshi.get_attribute_value(data, "ISAVAILABLE")
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def canvas_conversion(course, _resources = nil)
|
19
|
+
course
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class Module < Resource
|
5
|
+
attr_accessor :module_items
|
6
|
+
|
7
|
+
def initialize(title, identifier)
|
8
|
+
@identifier = identifier
|
9
|
+
@title = title
|
10
|
+
@module_items = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def canvas_conversion(*)
|
14
|
+
CanvasCc::CanvasCC::Models::CanvasModule.new.tap do |cc_module|
|
15
|
+
cc_module.identifier = @identifier
|
16
|
+
cc_module.title = @title
|
17
|
+
cc_module.workflow_state = "published"
|
18
|
+
cc_module.module_items = @module_items
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class ModuleItem < Resource
|
5
|
+
def initialize(title, type, identifierref)
|
6
|
+
@title = title
|
7
|
+
@identifier = Senkyoshi.create_random_hex
|
8
|
+
@content_type = type
|
9
|
+
@identifierref = identifierref
|
10
|
+
@workflow_state = "active"
|
11
|
+
end
|
12
|
+
|
13
|
+
def canvas_conversion(*)
|
14
|
+
CanvasCc::CanvasCC::Models::ModuleItem.new.tap do |item|
|
15
|
+
item.title = @title
|
16
|
+
item.identifier = @identifier
|
17
|
+
item.content_type = @content_type
|
18
|
+
item.identifierref = @identifierref
|
19
|
+
item.workflow_state = @workflow_state
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class Question < Resource
|
5
|
+
attr_reader :answers
|
6
|
+
|
7
|
+
QUESTION_TYPE = {
|
8
|
+
"True/False" => "true_false_question",
|
9
|
+
"Numeric" => "numerical_question",
|
10
|
+
"Multiple Choice" => "multiple_choice_question",
|
11
|
+
"Multiple Answer" => "multiple_answers_question",
|
12
|
+
"Matching" => "matching_question",
|
13
|
+
"Fill in the Blank" => "short_answer_question",
|
14
|
+
"Fill in the Blank Plus" => "fill_in_multiple_blanks_question",
|
15
|
+
"File Upload" => "file_upload_question",
|
16
|
+
"Essay" => "essay_question",
|
17
|
+
"Calculated" => "calculated_question",
|
18
|
+
"Jumbled Sentence" => "multiple_dropdowns_question",
|
19
|
+
"Either/Or" => "multiple_choice_question",
|
20
|
+
"Hot Spot" => "text_only_question",
|
21
|
+
"Opinion Scale" => "multiple_answers_question",
|
22
|
+
"Ordering" => "matching_question",
|
23
|
+
"Quiz Bowl" => "text_only_question",
|
24
|
+
"Short Response" => "essay_question",
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
ITEM_FUNCTION = {
|
28
|
+
"True/False" => "TrueFalse",
|
29
|
+
"Numeric" => "NumericalQuestion",
|
30
|
+
"Multiple Choice" => "MultipleChoice",
|
31
|
+
"Multiple Answer" => "MultipleAnswer",
|
32
|
+
"Matching" => "Matching",
|
33
|
+
"Fill in the Blank" => "FillInBlank",
|
34
|
+
"Fill in Multiple Blanks" => "FillInMultipleBlanks",
|
35
|
+
"Fill in the Blank Plus" => "FillInBlankPlus",
|
36
|
+
"File Upload" => "FileUpload",
|
37
|
+
"Essay" => "Essay",
|
38
|
+
"Calculated" => "Calculated",
|
39
|
+
"Jumbled Sentence" => "JumbledSentence",
|
40
|
+
"Either/Or" => "EitherOr",
|
41
|
+
"Hot Spot" => "HotSpot",
|
42
|
+
"Opinion Scale" => "OpinionScale",
|
43
|
+
"Ordering" => "Ordering",
|
44
|
+
"Quiz Bowl" => "QuizBowl",
|
45
|
+
"Short Response" => "ShortResponse",
|
46
|
+
}.freeze
|
47
|
+
|
48
|
+
def self.from(item)
|
49
|
+
type = item.at("bbmd_questiontype").children.text
|
50
|
+
item_class = Senkyoshi.const_get ITEM_FUNCTION[type]
|
51
|
+
question = item_class.new
|
52
|
+
question.iterate_xml(item)
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@original_identifier = ""
|
57
|
+
@question = nil
|
58
|
+
@question_type = ""
|
59
|
+
@points_possible = ""
|
60
|
+
@title = "Question"
|
61
|
+
@material = ""
|
62
|
+
@answers = []
|
63
|
+
@general_feedback = ""
|
64
|
+
@general_correct_feedback = ""
|
65
|
+
@general_incorrect_feedback = ""
|
66
|
+
@blackboard_type = ""
|
67
|
+
@correct_answers = {}
|
68
|
+
@incorrect_answers = {}
|
69
|
+
@max_score = 1
|
70
|
+
end
|
71
|
+
|
72
|
+
def iterate_xml(data)
|
73
|
+
@original_identifier = data.at("bbmd_asi_object_id").text
|
74
|
+
@blackboard_type = data.at("bbmd_questiontype").text
|
75
|
+
@question_type = QUESTION_TYPE[@blackboard_type]
|
76
|
+
@question = CanvasCc::CanvasCC::Models::Question.create(@question_type)
|
77
|
+
@points_possible = data.at("qmd_absolutescore_max").text
|
78
|
+
title = data.attributes["title"]
|
79
|
+
@title = title ? title.value : ""
|
80
|
+
iterate_item(data)
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def canvas_conversion(assessment, resources)
|
85
|
+
@question.identifier = Senkyoshi.create_random_hex
|
86
|
+
@question.title = @title
|
87
|
+
@question.points_possible = @points_possible
|
88
|
+
@question.material = fix_html(@material, resources)
|
89
|
+
@question.general_feedback = fix_html(@general_feedback, resources)
|
90
|
+
@general_correct_feedback =
|
91
|
+
fix_html(@general_correct_feedback, resources)
|
92
|
+
@question.general_correct_feedback = @general_correct_feedback
|
93
|
+
@general_incorrect_feedback =
|
94
|
+
fix_html(@general_incorrect_feedback, resources)
|
95
|
+
@question.general_incorrect_feedback = @general_incorrect_feedback
|
96
|
+
@question.answers = []
|
97
|
+
@answers.each do |answer|
|
98
|
+
@question = answer.canvas_conversion(@question, resources)
|
99
|
+
end
|
100
|
+
assessment.items << @question
|
101
|
+
assessment
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_fraction(answer_text)
|
105
|
+
if @correct_answers && answer_text == @correct_answers["name"]
|
106
|
+
@correct_answers["fraction"].to_f
|
107
|
+
else
|
108
|
+
@incorrect_answers["fraction"].to_f
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def set_answers(resprocessing)
|
113
|
+
set_correct_answers(resprocessing)
|
114
|
+
set_incorrect_answers(resprocessing)
|
115
|
+
end
|
116
|
+
|
117
|
+
def set_correct_answers(resprocessing)
|
118
|
+
correct = resprocessing.at("respcondition[title=correct]")
|
119
|
+
if correct
|
120
|
+
if correct.at("varequal")
|
121
|
+
@correct_answers["name"] = correct.at("varequal").text
|
122
|
+
end
|
123
|
+
score = correct.at("setvar") ? correct.at("setvar").text : 0
|
124
|
+
score_number = score == "SCORE.max" ? @max_score.to_f : score.to_f
|
125
|
+
if score_number > 0
|
126
|
+
@correct_answers["fraction"] = score_number.to_f / @max_score.to_f
|
127
|
+
else
|
128
|
+
@correct_answers["fraction"] = 0
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def set_incorrect_answers(resprocessing)
|
134
|
+
incorrect = resprocessing.at("respcondition[title=incorrect]")
|
135
|
+
if incorrect
|
136
|
+
if incorrect.at("varequal")
|
137
|
+
@incorrect_answers["name"] = incorrect.at("varequal").text
|
138
|
+
end
|
139
|
+
score = incorrect.at("setvar") ? incorrect.at("setvar").text : 0
|
140
|
+
score_number = score == "SCORE.max" ? @max_score.to_f : score.to_f
|
141
|
+
if score_number > 0
|
142
|
+
@incorrect_answers["fraction"] = score_number.to_f / @max_score.to_f
|
143
|
+
else
|
144
|
+
@incorrect_answers["fraction"] = 0
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def iterate_item(data)
|
150
|
+
@general_correct_feedback = set_feedback(data, "correct")
|
151
|
+
@general_incorrect_feedback = set_feedback(data, "incorrect")
|
152
|
+
@material = set_material(data)
|
153
|
+
resprocessing = data.at("resprocessing")
|
154
|
+
@max_score = set_max_score(resprocessing)
|
155
|
+
end
|
156
|
+
|
157
|
+
def set_feedback(data, type)
|
158
|
+
feedback = data.at("itemfeedback[ident=#{type}]")
|
159
|
+
if feedback && feedback.at("mat_formattedtext")
|
160
|
+
feedback.at("mat_formattedtext").text
|
161
|
+
else
|
162
|
+
""
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def set_material(data)
|
167
|
+
if (question_block = data.at("flow[@class=QUESTION_BLOCK]"))
|
168
|
+
question_block.at("mat_formattedtext").text
|
169
|
+
else
|
170
|
+
""
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def set_max_score(resprocessing)
|
175
|
+
no_score = "0.0"
|
176
|
+
outcomes = resprocessing.at("outcomes")
|
177
|
+
if outcomes && !outcomes.at("decvar").nil?
|
178
|
+
if outcomes.at("decvar").attributes["maxvalue"]
|
179
|
+
outcomes.at("decvar").attributes["maxvalue"].value
|
180
|
+
else
|
181
|
+
no_score
|
182
|
+
end
|
183
|
+
else
|
184
|
+
no_score
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Senkyoshi
|
2
|
+
class Calculated < Question
|
3
|
+
attr_reader :dataset_definitions, :var_sets
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@dataset_definitions = []
|
7
|
+
@var_sets = []
|
8
|
+
@correct_answer_length = 0
|
9
|
+
@correct_answer_format = 0
|
10
|
+
@tolerance = 0
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def canvas_conversion(*)
|
15
|
+
@question.dataset_definitions = @dataset_definitions
|
16
|
+
@question.var_sets = @var_sets
|
17
|
+
@question.correct_answer_length = @correct_answer_length
|
18
|
+
@question.correct_answer_format = @correct_answer_format
|
19
|
+
@question.tolerance = @tolerance
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def iterate_xml(data)
|
24
|
+
super
|
25
|
+
calculated_node = data.at("itemproc_extension > calculated")
|
26
|
+
math_ml = CGI.unescapeHTML(calculated_node.at("formula").text)
|
27
|
+
formula = Nokogiri::HTML(math_ml).text
|
28
|
+
|
29
|
+
answer = Answer.new(formula)
|
30
|
+
@answers.push(answer)
|
31
|
+
|
32
|
+
@tolerance = calculated_node.at("answer_tolerance").text
|
33
|
+
# canvas_cc only uses the correct_answer_length if the
|
34
|
+
# correct_answer_format is 1. It is not known what 1 represents.
|
35
|
+
@correct_answer_format = 1
|
36
|
+
@correct_answer_length = calculated_node.at("answer_scale").text
|
37
|
+
|
38
|
+
@dataset_definitions = _parse_vars(calculated_node.at("vars"))
|
39
|
+
@var_sets = _parse_var_sets(calculated_node.at("var_sets"))
|
40
|
+
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def _parse_vars(parent_node)
|
45
|
+
parent_node.search("var").map do |var|
|
46
|
+
scale = var.attributes["scale"]
|
47
|
+
min = var.at("min").text
|
48
|
+
max = var.at("max").text
|
49
|
+
options = ":#{min}:#{max}:#{scale}"
|
50
|
+
|
51
|
+
{
|
52
|
+
name: var.attributes["name"].value,
|
53
|
+
options: options,
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def _parse_var_sets(parent_node)
|
59
|
+
parent_node.search("var_set").map do |var_set|
|
60
|
+
vars = var_set.search("var").each_with_object({}) do |var, hash|
|
61
|
+
hash[var.attributes["name"].text] = var.text
|
62
|
+
end
|
63
|
+
|
64
|
+
{
|
65
|
+
ident: var_set.attributes["ident"].text,
|
66
|
+
answer: var_set.at("answer").text,
|
67
|
+
vars: vars,
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Senkyoshi
|
2
|
+
class EitherOr < Question
|
3
|
+
EITHER_OR = {
|
4
|
+
"yes_no.true" => "Yes",
|
5
|
+
"yes_no.false" => "No",
|
6
|
+
"agree_disagree.true" => "Agree",
|
7
|
+
"agree_disagree.false" => "Disagree",
|
8
|
+
"right_wrong.true" => "Right",
|
9
|
+
"right_wrong.false" => "Wrong",
|
10
|
+
"true_false.true" => "True",
|
11
|
+
"true_false.false" => "False",
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
super
|
16
|
+
@original_text = ""
|
17
|
+
end
|
18
|
+
|
19
|
+
def iterate_xml(data)
|
20
|
+
super
|
21
|
+
set_answers(data.at("resprocessing"))
|
22
|
+
data.at("flow_label").children.each do |response|
|
23
|
+
answer_text = response.at("mattext").text
|
24
|
+
answer = Answer.new(EITHER_OR[answer_text])
|
25
|
+
answer.fraction = get_fraction(answer_text)
|
26
|
+
@answers.push(answer)
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|