senkyoshi 1.0.1 → 1.0.2
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 +7 -2
- data/lib/senkyoshi/canvas_course.rb +15 -9
- data/lib/senkyoshi/models/assessment.rb +4 -93
- data/lib/senkyoshi/models/attachment.rb +21 -0
- data/lib/senkyoshi/models/content.rb +39 -18
- data/lib/senkyoshi/models/content_file.rb +21 -4
- data/lib/senkyoshi/models/external_url.rb +9 -0
- data/lib/senkyoshi/models/file.rb +22 -21
- data/lib/senkyoshi/models/gradebook.rb +5 -8
- data/lib/senkyoshi/models/module_item.rb +3 -1
- data/lib/senkyoshi/models/qti.rb +215 -0
- data/lib/senkyoshi/models/question.rb +3 -3
- data/lib/senkyoshi/models/question_bank.rb +46 -0
- data/lib/senkyoshi/models/questions/fill_in_blank.rb +3 -1
- data/lib/senkyoshi/models/questions/fill_in_blank_plus.rb +3 -1
- data/lib/senkyoshi/models/questions/matching.rb +8 -3
- data/lib/senkyoshi/models/questions/multiple_choice.rb +12 -0
- data/lib/senkyoshi/models/questions/numerical.rb +4 -1
- data/lib/senkyoshi/models/questions/true_false.rb +1 -1
- data/lib/senkyoshi/models/resource.rb +8 -6
- data/lib/senkyoshi/models/scorm_package.rb +51 -34
- data/lib/senkyoshi/models/staff_info.rb +49 -31
- data/lib/senkyoshi/models/survey.rb +10 -0
- data/lib/senkyoshi/models/wikipage.rb +55 -9
- data/lib/senkyoshi/version.rb +1 -2
- data/lib/senkyoshi/xml_parser.rb +60 -31
- data/lib/senkyoshi.rb +14 -8
- metadata +8 -3
@@ -0,0 +1,215 @@
|
|
1
|
+
require "senkyoshi/models/assignment_group"
|
2
|
+
require "senkyoshi/models/assignment"
|
3
|
+
require "senkyoshi/models/question"
|
4
|
+
require "senkyoshi/models/resource"
|
5
|
+
|
6
|
+
QTI_TYPE = {
|
7
|
+
"Test" => "Assessment",
|
8
|
+
"Survey" => "Survey",
|
9
|
+
"Pool" => "QuestionBank",
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
module Senkyoshi
|
13
|
+
class QTI < Resource
|
14
|
+
def self.from(data, pre_data)
|
15
|
+
type = data.at("bbmd_assessmenttype").text
|
16
|
+
qti_class = Senkyoshi.const_get QTI_TYPE[type]
|
17
|
+
qti = qti_class.new
|
18
|
+
qti.iterate_xml(data, pre_data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@title = ""
|
23
|
+
@description = ""
|
24
|
+
@quiz_type = ""
|
25
|
+
@points_possible = 0
|
26
|
+
@items = []
|
27
|
+
@group_name = ""
|
28
|
+
@workflow_state = "published"
|
29
|
+
@available = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def iterate_xml(data, pre_data)
|
33
|
+
@group_name = pre_data[:category] || data.at("bbmd_assessmenttype").text
|
34
|
+
@id = pre_data[:assignment_id] || pre_data[:file_name]
|
35
|
+
@title = data.at("assessment").attributes["title"].value
|
36
|
+
@points_possible = data.at("qmd_absolutescore_max").text
|
37
|
+
set_assessment_details(pre_data)
|
38
|
+
@due_at = pre_data[:due_at]
|
39
|
+
|
40
|
+
description = data.at("presentation_material").
|
41
|
+
at("mat_formattedtext").text
|
42
|
+
instructions = data.at("rubric").
|
43
|
+
at("mat_formattedtext").text
|
44
|
+
@description = %{
|
45
|
+
#{description}
|
46
|
+
#{instructions}
|
47
|
+
}
|
48
|
+
@items = data.search("item").to_a
|
49
|
+
@items += get_quiz_pool_items(data.search("selection_ordering"))
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_assessment_details(pre_data)
|
54
|
+
@time_limit = pre_data[:time_limit]
|
55
|
+
@allowed_attempts = pre_data[:allowed_attempts]
|
56
|
+
@allowed_attempts = -1 if pre_data[:unlimited_attempts] == "true"
|
57
|
+
@cant_go_back = pre_data[:cant_go_back]
|
58
|
+
@show_correct_answers = pre_data[:show_correct_answers]
|
59
|
+
if pre_data[:access_code] && !pre_data[:access_code].empty?
|
60
|
+
@access_code = pre_data[:access_code]
|
61
|
+
end
|
62
|
+
if pre_data[:one_question_at_a_time] == "QUESTION_BY_QUESTION"
|
63
|
+
@one_question_at_a_time = "true"
|
64
|
+
else
|
65
|
+
@one_question_at_a_time = "false"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_quiz_pool_items(selection_order)
|
70
|
+
selection_order.flat_map do |selection|
|
71
|
+
weight = selection.previous.at("qmd_weighting").text
|
72
|
+
selection_number = selection.at("selection_number").text
|
73
|
+
item = {
|
74
|
+
weight: weight,
|
75
|
+
selection_number: selection_number,
|
76
|
+
}
|
77
|
+
if selection.at("sourcebank_ref")
|
78
|
+
sourcebank_ref = selection.at("sourcebank_ref").text
|
79
|
+
item[:file_name] = sourcebank_ref
|
80
|
+
elsif selection.at("or_selection")
|
81
|
+
questions = selection.search("selection_metadata").map(&:text)
|
82
|
+
item[:questions] = questions
|
83
|
+
end
|
84
|
+
item
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_pre_data(xml, _)
|
89
|
+
{
|
90
|
+
original_file_name: xml.xpath("/COURSEASSESSMENT/
|
91
|
+
ASMTID/@value").first.text,
|
92
|
+
one_question_at_a_time: xml.xpath("/COURSEASSESSMENT/
|
93
|
+
DELIVERYTYPE/@value").first.text,
|
94
|
+
time_limit: xml.xpath("/COURSEASSESSMENT/
|
95
|
+
TIMELIMIT/@value").first.text,
|
96
|
+
access_code: xml.xpath("/COURSEASSESSMENT/
|
97
|
+
PASSWORD/@value").first.text,
|
98
|
+
allowed_attempts: xml.xpath("/COURSEASSESSMENT/
|
99
|
+
ATTEMPTCOUNT/@value").first.text,
|
100
|
+
unlimited_attempts: xml.xpath("/COURSEASSESSMENT/
|
101
|
+
FLAGS/ISUNLIMITEDATTEMPTS/@value").first.text,
|
102
|
+
cant_go_back: xml.xpath("/COURSEASSESSMENT/
|
103
|
+
FLAGS/ISBACKTRACKPROHIBITED/@value").first.text,
|
104
|
+
show_correct_answers: xml.xpath("/COURSEASSESSMENT/
|
105
|
+
FLAGS/SHOWCORRECTANSWER/@value").first.text,
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
def canvas_conversion(course, resources)
|
110
|
+
assessment = CanvasCc::CanvasCC::Models::Assessment.new
|
111
|
+
assessment.identifier = @id
|
112
|
+
course = create_assignment_group(course, resources)
|
113
|
+
assignment = create_assignment
|
114
|
+
assignment.quiz_identifier_ref = assessment.identifier
|
115
|
+
course.assignments << assignment
|
116
|
+
assessment = setup_assessment(assessment, assignment, resources)
|
117
|
+
assessment = create_items(course, assessment, resources)
|
118
|
+
course.assessments << assessment
|
119
|
+
course
|
120
|
+
end
|
121
|
+
|
122
|
+
def setup_assessment(assessment, assignment, resources)
|
123
|
+
assessment.title = @title
|
124
|
+
assessment.description = fix_html(@description, resources)
|
125
|
+
if @items.count.zero?
|
126
|
+
assessment.description +=
|
127
|
+
"Empty Quiz -- No questions were contained in the blackboard quiz"
|
128
|
+
end
|
129
|
+
assessment.available = @available
|
130
|
+
assessment.quiz_type = @quiz_type
|
131
|
+
assessment.points_possible = @points_possible
|
132
|
+
assessment.time_limit = @time_limit
|
133
|
+
assessment.access_code = @access_code
|
134
|
+
assessment.allowed_attempts = @allowed_attempts
|
135
|
+
assessment.cant_go_back = @cant_go_back
|
136
|
+
assessment.show_correct_answers = @show_correct_answers
|
137
|
+
assessment.one_question_at_a_time = @one_question_at_a_time
|
138
|
+
if @due_at && !@due_at.empty?
|
139
|
+
assessment.due_at = Time.strptime(@due_at, "%Y-%m-%d %H:%M:%S %z")
|
140
|
+
end
|
141
|
+
assessment.assignment = assignment
|
142
|
+
assessment
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_items(course, assessment, resources)
|
146
|
+
questions = get_questions(course)
|
147
|
+
assessment.items = []
|
148
|
+
questions.each do |item|
|
149
|
+
if canvas_module?(item)
|
150
|
+
assessment.items << item
|
151
|
+
else
|
152
|
+
assessment.items << item.canvas_conversion(assessment, resources)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
assessment
|
156
|
+
end
|
157
|
+
|
158
|
+
def get_questions(course)
|
159
|
+
@items = @items - ["", nil]
|
160
|
+
@items.flat_map do |item|
|
161
|
+
if !item[:selection_number]
|
162
|
+
Question.from(item)
|
163
|
+
else
|
164
|
+
get_question_group(course, item)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def canvas_module?(item)
|
170
|
+
item.is_a? CanvasCc::CanvasCC::Models::QuestionGroup
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_question_group(course, item)
|
174
|
+
if item[:questions]
|
175
|
+
questions = course.question_banks.flat_map(&:questions)
|
176
|
+
canvas_questions = item[:questions].flat_map do |question|
|
177
|
+
questions.detect { |q| q.original_identifier == question }
|
178
|
+
end.compact
|
179
|
+
end
|
180
|
+
question_group = CanvasCc::CanvasCC::Models::QuestionGroup.new
|
181
|
+
question_group.identifier = Senkyoshi.create_random_hex
|
182
|
+
question_group.title = "Question Group"
|
183
|
+
question_group.selection_number = item[:selection_number]
|
184
|
+
question_group.questions = canvas_questions || []
|
185
|
+
question_group.sourcebank_ref = item[:file_name]
|
186
|
+
question_group.points_per_item = item[:weight]
|
187
|
+
question_group
|
188
|
+
end
|
189
|
+
|
190
|
+
def create_assignment_group(course, resources)
|
191
|
+
group = course.assignment_groups.detect { |a| a.title == @group_name }
|
192
|
+
if group
|
193
|
+
@group_id = group.identifier
|
194
|
+
else
|
195
|
+
@group_id = Senkyoshi.create_random_hex
|
196
|
+
assignment_group = AssignmentGroup.new(@group_name, @group_id)
|
197
|
+
course = assignment_group.canvas_conversion(course, resources)
|
198
|
+
end
|
199
|
+
course
|
200
|
+
end
|
201
|
+
|
202
|
+
def create_assignment
|
203
|
+
assignment = CanvasCc::CanvasCC::Models::Assignment.new
|
204
|
+
assignment.identifier = Senkyoshi.create_random_hex
|
205
|
+
assignment.assignment_group_identifier_ref = @group_id
|
206
|
+
assignment.title = @title
|
207
|
+
assignment.position = 1
|
208
|
+
assignment.submission_types << "online_quiz"
|
209
|
+
assignment.grading_type = "points"
|
210
|
+
assignment.workflow_state = @workflow_state
|
211
|
+
assignment.points_possible = @points_possible
|
212
|
+
assignment
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -81,9 +81,10 @@ module Senkyoshi
|
|
81
81
|
self
|
82
82
|
end
|
83
83
|
|
84
|
-
def canvas_conversion(
|
84
|
+
def canvas_conversion(_, resources)
|
85
85
|
@question.identifier = Senkyoshi.create_random_hex
|
86
86
|
@question.title = @title
|
87
|
+
@question.original_identifier = @original_identifier
|
87
88
|
@question.points_possible = @points_possible
|
88
89
|
@question.material = fix_html(@material, resources)
|
89
90
|
@question.general_feedback = fix_html(@general_feedback, resources)
|
@@ -97,8 +98,7 @@ module Senkyoshi
|
|
97
98
|
@answers.each do |answer|
|
98
99
|
@question = answer.canvas_conversion(@question, resources)
|
99
100
|
end
|
100
|
-
|
101
|
-
assessment
|
101
|
+
@question
|
102
102
|
end
|
103
103
|
|
104
104
|
def get_fraction(answer_text)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "senkyoshi/models/qti"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class QuestionBank < QTI
|
5
|
+
def canvas_conversion(course, resources)
|
6
|
+
question_bank = CanvasCc::CanvasCC::Models::QuestionBank.new
|
7
|
+
question_bank.identifier = @id
|
8
|
+
question_bank.title = @title
|
9
|
+
question_bank = setup_question_bank(question_bank, resources)
|
10
|
+
course.question_banks << question_bank
|
11
|
+
course
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup_question_bank(question_bank, resources)
|
15
|
+
if @items.count.zero?
|
16
|
+
question_bank.description += "Empty Quiz -- No questions
|
17
|
+
were contained in the blackboard quiz bank"
|
18
|
+
end
|
19
|
+
question_bank = create_items(question_bank, resources)
|
20
|
+
question_bank
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_items(question_bank, resources)
|
24
|
+
@items = @items - ["", nil]
|
25
|
+
questions = @items.map do |item|
|
26
|
+
Question.from(item)
|
27
|
+
end
|
28
|
+
question_bank.questions = []
|
29
|
+
questions.each do |item|
|
30
|
+
question = item.canvas_conversion(question_bank, resources)
|
31
|
+
question.material = clean_up_material(question)
|
32
|
+
question_bank.questions << question
|
33
|
+
end
|
34
|
+
question_bank
|
35
|
+
end
|
36
|
+
|
37
|
+
# This is to remove the random extra <p>.</p> included in the
|
38
|
+
# description that is just randomly there
|
39
|
+
def clean_up_material(question)
|
40
|
+
tag = "<p><span size=\"2\" style=\"font-size: small;\">.</span></p>"
|
41
|
+
question.material.gsub!(tag, "")
|
42
|
+
question.material.gsub!("<p>.</p>", "")
|
43
|
+
question.material.strip!
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -4,7 +4,9 @@ module Senkyoshi
|
|
4
4
|
class FillInBlank < Question
|
5
5
|
def iterate_xml(data)
|
6
6
|
super
|
7
|
-
|
7
|
+
if data.at("resprocessing")
|
8
|
+
conditionvar = data.at("resprocessing").at("conditionvar")
|
9
|
+
end
|
8
10
|
# not all fill in the blank questions have answers(ie: surveys)
|
9
11
|
if conditionvar
|
10
12
|
answer = Answer.new(conditionvar.at("varequal").text)
|
@@ -4,7 +4,9 @@ module Senkyoshi
|
|
4
4
|
class FillInBlankPlus < Question
|
5
5
|
def iterate_xml(data)
|
6
6
|
super
|
7
|
-
|
7
|
+
if data.at("resprocessing")
|
8
|
+
conditionvar = data.at("resprocessing").at("conditionvar")
|
9
|
+
end
|
8
10
|
# not all fill in the blank questions have answers(ie: surveys)
|
9
11
|
if conditionvar
|
10
12
|
conditionvar.at("and").children.each do |or_child|
|
@@ -6,6 +6,7 @@ module Senkyoshi
|
|
6
6
|
super
|
7
7
|
@matches = []
|
8
8
|
@matching_answers = {}
|
9
|
+
@distractors = []
|
9
10
|
end
|
10
11
|
|
11
12
|
def iterate_xml(data)
|
@@ -13,10 +14,11 @@ module Senkyoshi
|
|
13
14
|
resprocessing = data.at("resprocessing")
|
14
15
|
@matching_answers = set_matching_answers(resprocessing)
|
15
16
|
matches_array = []
|
17
|
+
answers = []
|
16
18
|
if match_block = data.at("flow[@class=RIGHT_MATCH_BLOCK]")
|
17
|
-
matches_array = match_block.
|
18
|
-
|
19
|
-
|
19
|
+
matches_array = match_block.
|
20
|
+
search("flow[@class=FORMATTED_TEXT_BLOCK]").
|
21
|
+
map(&:text)
|
20
22
|
end
|
21
23
|
if response_block = data.at("flow[@class=RESPONSE_BLOCK]")
|
22
24
|
response_block.children.each do |response|
|
@@ -30,14 +32,17 @@ module Senkyoshi
|
|
30
32
|
answer = matches_array[index]
|
31
33
|
end
|
32
34
|
end
|
35
|
+
answers << answer
|
33
36
|
@matches << { id: id, question_text: question, answer_text: answer }
|
34
37
|
end
|
35
38
|
end
|
39
|
+
@distractors = matches_array.reject { |i| answers.include?(i) }
|
36
40
|
self
|
37
41
|
end
|
38
42
|
|
39
43
|
def canvas_conversion(assessment, _resources = nil)
|
40
44
|
@question.matches = @matches
|
45
|
+
@question.distractors = @distractors
|
41
46
|
super
|
42
47
|
end
|
43
48
|
|
@@ -16,5 +16,17 @@ module Senkyoshi
|
|
16
16
|
end
|
17
17
|
self
|
18
18
|
end
|
19
|
+
|
20
|
+
def get_fraction(answer_text)
|
21
|
+
if @correct_answers && answer_text == @correct_answers["name"]
|
22
|
+
if @correct_answers["fraction"].to_f == 0.0
|
23
|
+
1.0
|
24
|
+
else
|
25
|
+
@correct_answers["fraction"].to_f
|
26
|
+
end
|
27
|
+
else
|
28
|
+
@incorrect_answers["fraction"].to_f
|
29
|
+
end
|
30
|
+
end
|
19
31
|
end
|
20
32
|
end
|
@@ -10,7 +10,10 @@ module Senkyoshi
|
|
10
10
|
|
11
11
|
def iterate_xml(data)
|
12
12
|
super
|
13
|
-
|
13
|
+
if data.at("resprocessing")
|
14
|
+
conditionvar = data.at("resprocessing").at("conditionvar")
|
15
|
+
end
|
16
|
+
|
14
17
|
if conditionvar
|
15
18
|
range = CanvasCc::CanvasCC::Models::Range.new
|
16
19
|
range.low_range = conditionvar.at("vargte").text.to_i
|
@@ -8,7 +8,7 @@ module Senkyoshi
|
|
8
8
|
set_answers(data.at("resprocessing"))
|
9
9
|
answers_array.each do |answer_text|
|
10
10
|
answer = Answer.new(answer_text)
|
11
|
-
answer.fraction = get_fraction(answer_text)
|
11
|
+
answer.fraction = get_fraction(answer_text.to_s)
|
12
12
|
@answers.push(answer)
|
13
13
|
end
|
14
14
|
self
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Senkyoshi
|
2
2
|
class Resource
|
3
|
+
def cleanup; end
|
4
|
+
|
3
5
|
def fix_html(contents, resources)
|
4
6
|
if contents && contents.respond_to?(:empty?) && !contents.empty?
|
5
7
|
node_html = Nokogiri::HTML.fragment(contents)
|
@@ -20,12 +22,12 @@ module Senkyoshi
|
|
20
22
|
def _search_and_replace(resources, node_html, tag, attr)
|
21
23
|
node_html.search(tag).each do |element|
|
22
24
|
original_src = element[attr]
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
if original_src
|
26
|
+
xid = original_src.split("/").last
|
27
|
+
file_resource = resources.detect_xid(xid)
|
28
|
+
if file_resource
|
29
|
+
element[attr] = "#{BASE}/#{file_resource.path}"
|
30
|
+
end
|
29
31
|
end
|
30
32
|
end
|
31
33
|
end
|
@@ -1,52 +1,70 @@
|
|
1
1
|
module Senkyoshi
|
2
2
|
class ScormPackage
|
3
|
-
attr_accessor(:entries, :manifest)
|
3
|
+
attr_accessor(:entries, :manifest, :points_possible)
|
4
4
|
|
5
|
-
|
6
|
-
# Scorm packages should include this string in the <schema> tag. We
|
7
|
-
# downcase, and remove spaces before checking to see if a manifest contains
|
8
|
-
# this schema to determine whether or not it belongs to a scorm package
|
9
|
-
##
|
10
|
-
SCORM_SCHEMA = "adlscorm".freeze
|
11
|
-
|
12
|
-
def initialize(zip_file, manifest)
|
5
|
+
def initialize(zip_file, manifest, scorm_item = nil)
|
13
6
|
@manifest = manifest
|
14
7
|
@entries = ScormPackage.get_entries zip_file, manifest
|
8
|
+
@points_possible = if scorm_item
|
9
|
+
scorm_item.xpath(
|
10
|
+
"/scormItem/gradebookInfo/@pointsPossible",
|
11
|
+
).text
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Extracts scorm packages from a blackboard export zip file
|
17
|
+
##
|
18
|
+
def self.get_scorm_packages(blackboard_export)
|
19
|
+
find_scorm_items(blackboard_export).
|
20
|
+
map do |item|
|
21
|
+
manifest_entry = find_scorm_manifest(blackboard_export, item)
|
22
|
+
ScormPackage.new blackboard_export, manifest_entry, item
|
23
|
+
end
|
15
24
|
end
|
16
25
|
|
17
26
|
##
|
18
|
-
# Returns
|
19
|
-
##
|
20
|
-
def self.
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
# Returns paths to scormItem files
|
28
|
+
##
|
29
|
+
def self.find_scorm_item_paths(zip_file)
|
30
|
+
Nokogiri::XML.parse(
|
31
|
+
Senkyoshi.read_file(zip_file, "imsmanifest.xml"),
|
32
|
+
).
|
33
|
+
xpath("//resource[@type='resource/x-plugin-scormengine']").
|
34
|
+
map { |r| r.xpath("./@bb:file").text }
|
35
|
+
rescue Exceptions::MissingFileError => e
|
36
|
+
if zip_file
|
37
|
+
STDERR.puts "Blackboard export manifest file missing: #{zip_file.name}"
|
38
|
+
end
|
30
39
|
STDERR.puts e.to_s
|
31
|
-
|
40
|
+
|
41
|
+
[]
|
32
42
|
end
|
33
43
|
|
34
44
|
##
|
35
|
-
#
|
45
|
+
# Returns array of parsed scormItem files
|
36
46
|
##
|
37
|
-
def self.
|
38
|
-
|
39
|
-
|
47
|
+
def self.find_scorm_items(zip_file)
|
48
|
+
find_scorm_item_paths(zip_file).map do |path|
|
49
|
+
Nokogiri::XML.parse Senkyoshi.read_file(zip_file, path)
|
40
50
|
end
|
41
51
|
end
|
42
52
|
|
53
|
+
##
|
54
|
+
# Returns the zip file entry for the scorm package manifest, given
|
55
|
+
# a scormItem file
|
56
|
+
##
|
57
|
+
def self.find_scorm_manifest(zip_file, scorm_item)
|
58
|
+
path = scorm_item.xpath("/scormItem/@mappedContentId").text
|
59
|
+
zip_file.get_entry("#{path}/imsmanifest.xml")
|
60
|
+
end
|
61
|
+
|
43
62
|
##
|
44
63
|
# Returns array of all scorm manifest files inside of blackboard export
|
45
64
|
##
|
46
65
|
def self.find_scorm_manifests(zip_file)
|
47
|
-
|
48
|
-
|
49
|
-
File.fnmatch("*imsmanifest.xml", e.name) && scorm_manifest?(e)
|
66
|
+
find_scorm_items(zip_file).map do |scorm_item|
|
67
|
+
find_scorm_manifest(zip_file, scorm_item)
|
50
68
|
end
|
51
69
|
end
|
52
70
|
|
@@ -81,9 +99,9 @@ module Senkyoshi
|
|
81
99
|
# location of temporary file
|
82
100
|
##
|
83
101
|
def write_zip(export_name)
|
84
|
-
|
102
|
+
@dir ||= Dir.mktmpdir
|
85
103
|
scorm_path = File.dirname @manifest.name
|
86
|
-
path = "#{
|
104
|
+
path = "#{@dir}/#{export_name}"
|
87
105
|
Zip::File.open path, Zip::File::CREATE do |zip|
|
88
106
|
@entries.each do |entry|
|
89
107
|
if entry.file?
|
@@ -101,9 +119,8 @@ module Senkyoshi
|
|
101
119
|
##
|
102
120
|
# Removes all temp files if they exist
|
103
121
|
##
|
104
|
-
def
|
105
|
-
|
106
|
-
FileUtils.rm_r @@dir unless @@dir.nil?
|
122
|
+
def cleanup
|
123
|
+
FileUtils.rm_r @dir unless @dir.nil?
|
107
124
|
end
|
108
125
|
end
|
109
126
|
end
|
@@ -1,20 +1,18 @@
|
|
1
1
|
require "senkyoshi/models/resource"
|
2
|
+
require "active_support/core_ext/string"
|
2
3
|
|
3
4
|
module Senkyoshi
|
4
5
|
class StaffInfo < Resource
|
5
6
|
attr_reader(
|
6
7
|
:id,
|
7
8
|
:title,
|
8
|
-
:
|
9
|
-
:name,
|
10
|
-
:email,
|
11
|
-
:phone,
|
12
|
-
:office_hours,
|
13
|
-
:office_address,
|
14
|
-
:home_page,
|
15
|
-
:image,
|
9
|
+
:entries,
|
16
10
|
)
|
17
11
|
|
12
|
+
def initialize
|
13
|
+
@entries = []
|
14
|
+
end
|
15
|
+
|
18
16
|
def parse_name(contact)
|
19
17
|
parts = [
|
20
18
|
contact.xpath("./NAME/FORMALTITLE/@value").text,
|
@@ -32,36 +30,56 @@ module Senkyoshi
|
|
32
30
|
|
33
31
|
def iterate_xml(xml, _pre_data)
|
34
32
|
contact = xml.xpath("//CONTACT")
|
35
|
-
@id
|
36
|
-
@title
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
33
|
+
@id ||= xml.xpath("//STAFFINFO/@id").text || Senkyoshi.create_random_hex
|
34
|
+
@title ||= xml.xpath("//STAFFINFO/TITLE/@value").text
|
35
|
+
bio = xml.xpath("//BIOGRAPHY/TEXT").text
|
36
|
+
name = parse_name(contact)
|
37
|
+
email = xml.xpath("//CONTACT/EMAIL/@value").text
|
38
|
+
phone = xml.xpath("//CONTACT/PHONE/@value").text
|
39
|
+
office_hours = xml.xpath("//OFFICE/HOURS/@value").text
|
40
|
+
office_address = xml.xpath("//OFFICE/ADDRESS/@value").text
|
41
|
+
home_page = xml.xpath("//HOMEPAGE/@value").text
|
42
|
+
image = xml.xpath("//IMAGE/@value").text
|
43
|
+
|
44
|
+
@entries << construct_body(
|
45
|
+
bio: bio,
|
46
|
+
name: name,
|
47
|
+
email: email,
|
48
|
+
phone: phone,
|
49
|
+
office_hours: office_hours,
|
50
|
+
office_address: office_address,
|
51
|
+
home_page: home_page,
|
52
|
+
image: image,
|
53
|
+
)
|
45
54
|
|
46
55
|
self
|
47
56
|
end
|
48
57
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
def append_str(body, str, var)
|
59
|
+
body << str if var && !var.empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
def humanize(symbol)
|
63
|
+
symbol.to_s.humanize.titleize
|
64
|
+
end
|
65
|
+
|
66
|
+
def construct_body(opts)
|
67
|
+
body = "<div>"
|
68
|
+
append_str body, "<img src=#{opts[:image]}/>", opts[:image]
|
69
|
+
append_str body, "<h3>#{opts[:name]}</h3>", opts[:name]
|
70
|
+
append_str body, "<p>#{opts[:bio]}</p>", opts[:bio]
|
71
|
+
|
72
|
+
body << "<ul>"
|
73
|
+
[:email, :phone, :office_hours, :office_address, :home_page].each do |key|
|
74
|
+
append_str body, "<li>#{humanize(key)}: #{opts[key]}</li>", opts[key]
|
75
|
+
end
|
76
|
+
body << "</ul></div>"
|
77
|
+
body
|
60
78
|
end
|
61
79
|
|
62
|
-
def canvas_conversion(course,
|
80
|
+
def canvas_conversion(course, resources)
|
63
81
|
page = CanvasCc::CanvasCC::Models::Page.new
|
64
|
-
page.body =
|
82
|
+
page.body = fix_html(@entries.join(" "), resources)
|
65
83
|
page.identifier = @id
|
66
84
|
page.page_name = @title.empty? ? "Contact" : @title
|
67
85
|
|