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