senkyoshi 1.0.0

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