senkyoshi 1.0.0

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +92 -0
  4. data/bin/console +14 -0
  5. data/bin/import_blackboard +6 -0
  6. data/lib/senkyoshi/canvas_course.rb +179 -0
  7. data/lib/senkyoshi/collection.rb +26 -0
  8. data/lib/senkyoshi/config.rb +35 -0
  9. data/lib/senkyoshi/exceptions.rb +13 -0
  10. data/lib/senkyoshi/models/announcement.rb +36 -0
  11. data/lib/senkyoshi/models/answer.rb +32 -0
  12. data/lib/senkyoshi/models/assessment.rb +95 -0
  13. data/lib/senkyoshi/models/assignment.rb +26 -0
  14. data/lib/senkyoshi/models/assignment_group.rb +23 -0
  15. data/lib/senkyoshi/models/blog.rb +22 -0
  16. data/lib/senkyoshi/models/content.rb +92 -0
  17. data/lib/senkyoshi/models/content_file.rb +26 -0
  18. data/lib/senkyoshi/models/course.rb +39 -0
  19. data/lib/senkyoshi/models/file.rb +99 -0
  20. data/lib/senkyoshi/models/forum.rb +28 -0
  21. data/lib/senkyoshi/models/gradebook.rb +30 -0
  22. data/lib/senkyoshi/models/group.rb +22 -0
  23. data/lib/senkyoshi/models/module.rb +22 -0
  24. data/lib/senkyoshi/models/module_item.rb +23 -0
  25. data/lib/senkyoshi/models/question.rb +188 -0
  26. data/lib/senkyoshi/models/questions/calculated.rb +72 -0
  27. data/lib/senkyoshi/models/questions/either_or.rb +31 -0
  28. data/lib/senkyoshi/models/questions/essay.rb +11 -0
  29. data/lib/senkyoshi/models/questions/file_upload.rb +4 -0
  30. data/lib/senkyoshi/models/questions/fill_in_blank.rb +15 -0
  31. data/lib/senkyoshi/models/questions/fill_in_blank_plus.rb +20 -0
  32. data/lib/senkyoshi/models/questions/hot_spot.rb +11 -0
  33. data/lib/senkyoshi/models/questions/jumbled_sentence.rb +37 -0
  34. data/lib/senkyoshi/models/questions/matching.rb +57 -0
  35. data/lib/senkyoshi/models/questions/multiple_answer.rb +56 -0
  36. data/lib/senkyoshi/models/questions/multiple_choice.rb +18 -0
  37. data/lib/senkyoshi/models/questions/numerical.rb +31 -0
  38. data/lib/senkyoshi/models/questions/opinion_scale.rb +18 -0
  39. data/lib/senkyoshi/models/questions/ordering.rb +40 -0
  40. data/lib/senkyoshi/models/questions/quiz_bowl.rb +11 -0
  41. data/lib/senkyoshi/models/questions/short_response.rb +4 -0
  42. data/lib/senkyoshi/models/questions/true_false.rb +15 -0
  43. data/lib/senkyoshi/models/quiz.rb +7 -0
  44. data/lib/senkyoshi/models/resource.rb +33 -0
  45. data/lib/senkyoshi/models/scorm_package.rb +109 -0
  46. data/lib/senkyoshi/models/staff_info.rb +72 -0
  47. data/lib/senkyoshi/models/wikipage.rb +34 -0
  48. data/lib/senkyoshi/tasks.rb +90 -0
  49. data/lib/senkyoshi/version.rb +3 -0
  50. data/lib/senkyoshi/xml_parser.rb +154 -0
  51. data/lib/senkyoshi.rb +69 -0
  52. 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&amp;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
@@ -0,0 +1,11 @@
1
+ module Senkyoshi
2
+ class Essay < Question
3
+ def iterate_xml(data)
4
+ super
5
+ itemfeedback = data.at("itemfeedback[ident=solution]")
6
+ feedback = itemfeedback.at("mat_formattedtext").text
7
+ @general_feedback = feedback
8
+ self
9
+ end
10
+ end
11
+ end