senkyoshi 1.0.1 → 1.0.2

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