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.
@@ -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(assessment, resources)
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
- assessment.items << @question
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
- conditionvar = data.at("resprocessing").at("conditionvar")
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
- conditionvar = data.at("resprocessing").at("conditionvar")
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.children.map do |match|
18
- match.at("mat_formattedtext").text
19
- end
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
- conditionvar = data.at("resprocessing").at("conditionvar")
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
- xid = original_src.split("/").last
24
- file_resource = resources.detect_xid(xid)
25
-
26
- if file_resource
27
- name = file_resource.name
28
- element[attr] = "#{BASE}/#{IMPORTED_FILES_DIRNAME}/#{name}"
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 true if a manifest is a scorm manifest file, false otherwise
19
- ##
20
- def self.scorm_manifest?(manifest)
21
- parsed_manifest = Nokogiri::XML(manifest.get_input_stream.read)
22
- schema_name = parsed_manifest.
23
- xpath("//xmlns:metadata/xmlns:schema").
24
- text.delete(" ").downcase
25
- return schema_name == SCORM_SCHEMA
26
- # NOTE we occasionally run into malformed manifest files
27
- rescue Nokogiri::XML::XPath::SyntaxError => e
28
- filename = manifest.zipfile
29
- STDERR.puts "Malformed scorm manifest found: #{manifest} in #{filename}"
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
- false
40
+
41
+ []
32
42
  end
33
43
 
34
44
  ##
35
- # Extracts scorm packages from a blackboard export zip file
45
+ # Returns array of parsed scormItem files
36
46
  ##
37
- def self.get_scorm_packages(blackboard_export)
38
- find_scorm_manifests(blackboard_export).map do |manifest|
39
- ScormPackage.new blackboard_export, manifest
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
- return [] if zip_file.nil?
48
- zip_file.entries.select do |e|
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
- @@dir ||= Dir.mktmpdir
102
+ @dir ||= Dir.mktmpdir
85
103
  scorm_path = File.dirname @manifest.name
86
- path = "#{@@dir}/#{export_name}"
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 self.cleanup
105
- @@dir ||= nil
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
- :bio,
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 = xml.xpath("//STAFFINFO/@id").text
36
- @title = xml.xpath("//STAFFINFO/TITLE/@value").text
37
- @bio = xml.xpath("//BIOGRAPHY/TEXT").text
38
- @name = parse_name(contact)
39
- @email = xml.xpath("//CONTACT/EMAIL/@value").text
40
- @phone = xml.xpath("//CONTACT/PHONE/@value").text
41
- @office_hours = xml.xpath("//OFFICE/HOURS/@value").text
42
- @office_address = xml.xpath("//OFFICE/ADDRESS/@value").text
43
- @home_page = xml.xpath("//HOMEPAGE/@value").text
44
- @image = xml.xpath("//IMAGE/@value").text
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 construct_body
50
- <<-HTML
51
- <h3>#{@name}</h3>
52
- <p>#{@bio}</p>
53
- <ul>
54
- <li>Email: #{@email}</li>
55
- <li>Phone: #{@phone}</li>
56
- <li>Office Hours: #{@office_hours}</li>
57
- <li>Office Address: #{@office_address}</li>
58
- </ul>
59
- HTML
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, _resources = nil)
80
+ def canvas_conversion(course, resources)
63
81
  page = CanvasCc::CanvasCC::Models::Page.new
64
- page.body = construct_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
 
@@ -0,0 +1,10 @@
1
+ require "senkyoshi/models/qti"
2
+
3
+ module Senkyoshi
4
+ class Survey < QTI
5
+ def iterate_xml(data, pre_data)
6
+ @quiz_type = "graded_survey"
7
+ super
8
+ end
9
+ end
10
+ end