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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +92 -0
- data/bin/console +14 -0
- data/bin/import_blackboard +6 -0
- data/lib/senkyoshi/canvas_course.rb +179 -0
- data/lib/senkyoshi/collection.rb +26 -0
- data/lib/senkyoshi/config.rb +35 -0
- data/lib/senkyoshi/exceptions.rb +13 -0
- data/lib/senkyoshi/models/announcement.rb +36 -0
- data/lib/senkyoshi/models/answer.rb +32 -0
- data/lib/senkyoshi/models/assessment.rb +95 -0
- data/lib/senkyoshi/models/assignment.rb +26 -0
- data/lib/senkyoshi/models/assignment_group.rb +23 -0
- data/lib/senkyoshi/models/blog.rb +22 -0
- data/lib/senkyoshi/models/content.rb +92 -0
- data/lib/senkyoshi/models/content_file.rb +26 -0
- data/lib/senkyoshi/models/course.rb +39 -0
- data/lib/senkyoshi/models/file.rb +99 -0
- data/lib/senkyoshi/models/forum.rb +28 -0
- data/lib/senkyoshi/models/gradebook.rb +30 -0
- data/lib/senkyoshi/models/group.rb +22 -0
- data/lib/senkyoshi/models/module.rb +22 -0
- data/lib/senkyoshi/models/module_item.rb +23 -0
- data/lib/senkyoshi/models/question.rb +188 -0
- data/lib/senkyoshi/models/questions/calculated.rb +72 -0
- data/lib/senkyoshi/models/questions/either_or.rb +31 -0
- data/lib/senkyoshi/models/questions/essay.rb +11 -0
- data/lib/senkyoshi/models/questions/file_upload.rb +4 -0
- data/lib/senkyoshi/models/questions/fill_in_blank.rb +15 -0
- data/lib/senkyoshi/models/questions/fill_in_blank_plus.rb +20 -0
- data/lib/senkyoshi/models/questions/hot_spot.rb +11 -0
- data/lib/senkyoshi/models/questions/jumbled_sentence.rb +37 -0
- data/lib/senkyoshi/models/questions/matching.rb +57 -0
- data/lib/senkyoshi/models/questions/multiple_answer.rb +56 -0
- data/lib/senkyoshi/models/questions/multiple_choice.rb +18 -0
- data/lib/senkyoshi/models/questions/numerical.rb +31 -0
- data/lib/senkyoshi/models/questions/opinion_scale.rb +18 -0
- data/lib/senkyoshi/models/questions/ordering.rb +40 -0
- data/lib/senkyoshi/models/questions/quiz_bowl.rb +11 -0
- data/lib/senkyoshi/models/questions/short_response.rb +4 -0
- data/lib/senkyoshi/models/questions/true_false.rb +15 -0
- data/lib/senkyoshi/models/quiz.rb +7 -0
- data/lib/senkyoshi/models/resource.rb +33 -0
- data/lib/senkyoshi/models/scorm_package.rb +109 -0
- data/lib/senkyoshi/models/staff_info.rb +72 -0
- data/lib/senkyoshi/models/wikipage.rb +34 -0
- data/lib/senkyoshi/tasks.rb +90 -0
- data/lib/senkyoshi/version.rb +3 -0
- data/lib/senkyoshi/xml_parser.rb +154 -0
- data/lib/senkyoshi.rb +69 -0
- 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&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
         |