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