canvas_qti_to_learnosity_converter 2.4.1 → 3.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 +4 -4
- data/canvas_qti_to_learnosity_converter.gemspec +1 -0
- data/lib/canvas_qti_to_learnosity_converter/convert.rb +302 -156
- data/lib/canvas_qti_to_learnosity_converter/export_writer.rb +21 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/calculated.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/essay.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/file_upload.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/fill_the_blanks.rb +4 -5
- data/lib/canvas_qti_to_learnosity_converter/questions/matching.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/multiple_choice.rb +6 -6
- data/lib/canvas_qti_to_learnosity_converter/questions/numerical.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/question.rb +29 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/short_answer.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/template_question.rb +4 -5
- data/lib/canvas_qti_to_learnosity_converter/questions/text_only.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/version.rb +1 -1
- data/lib/canvas_qti_to_learnosity_converter.rb +0 -1
- metadata +17 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: b87553f49f64e8fb54d2334edf92b467322364f3b32d8fa169c6f31cfef2d4be
         | 
| 4 | 
            +
              data.tar.gz: 502a402267786ee374ced98d282bc4774b69243bee341e2ed35bd051dd6e7db9
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 13688b93f41f22b5bfb9d1ef9a73c50828fa4c43f8a0c8b5e7102f3d5842b9d47d9735beaa5991babfc44374f3cb204d43a3a6d366ba71e6a13cbc8006ea4362
         | 
| 7 | 
            +
              data.tar.gz: f74370f50a3e2a9c7052bc0e82a48ec6ed08ebc05151ffdc463768ff122b75cefafba0ee538b756e27f78ce6bf0fd3ce98539816cdefaa475a3aa0687e4c757c
         | 
| @@ -3,7 +3,12 @@ require "forwardable" | |
| 3 3 | 
             
            require "ostruct"
         | 
| 4 4 | 
             
            require "zip"
         | 
| 5 5 | 
             
            require "uri"
         | 
| 6 | 
            +
            require "active_support"
         | 
| 7 | 
            +
            require "active_support/core_ext/digest/uuid"
         | 
| 8 | 
            +
            require "active_support/core_ext/securerandom"
         | 
| 9 | 
            +
            require "active_support/core_ext/object"
         | 
| 6 10 |  | 
| 11 | 
            +
            require "canvas_qti_to_learnosity_converter/export_writer"
         | 
| 7 12 | 
             
            require "canvas_qti_to_learnosity_converter/questions/multiple_choice"
         | 
| 8 13 | 
             
            require "canvas_qti_to_learnosity_converter/questions/short_answer"
         | 
| 9 14 | 
             
            require "canvas_qti_to_learnosity_converter/questions/fill_the_blanks"
         | 
| @@ -16,192 +21,333 @@ require "canvas_qti_to_learnosity_converter/questions/numerical" | |
| 16 21 | 
             
            require "canvas_qti_to_learnosity_converter/questions/calculated"
         | 
| 17 22 |  | 
| 18 23 | 
             
            module CanvasQtiToLearnosityConverter
         | 
| 19 | 
            -
               | 
| 20 | 
            -
             | 
| 21 | 
            -
                 | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
                 | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
                   | 
| 51 | 
            -
                   | 
| 24 | 
            +
              class Converter
         | 
| 25 | 
            +
                FEATURE_TYPES = [ :text_only_question ]
         | 
| 26 | 
            +
                QUESTION_TYPES = [
         | 
| 27 | 
            +
                  :multiple_choice_question,
         | 
| 28 | 
            +
                  :true_false_question,
         | 
| 29 | 
            +
                  :multiple_answers_question,
         | 
| 30 | 
            +
                  :short_answer_question,
         | 
| 31 | 
            +
                  :fill_in_multiple_blanks_question,
         | 
| 32 | 
            +
                  :multiple_dropdowns_question,
         | 
| 33 | 
            +
                  :matching_question,
         | 
| 34 | 
            +
                  :essay_question,
         | 
| 35 | 
            +
                  :file_upload_question,
         | 
| 36 | 
            +
                ]
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                TYPE_MAP = {
         | 
| 39 | 
            +
                  multiple_choice_question: MultipleChoiceQuestion,
         | 
| 40 | 
            +
                  true_false_question: MultipleChoiceQuestion,
         | 
| 41 | 
            +
                  multiple_answers_question: MultipleAnswersQuestion,
         | 
| 42 | 
            +
                  short_answer_question: ShortAnswerQuestion,
         | 
| 43 | 
            +
                  fill_in_multiple_blanks_question: FillTheBlanksQuestion,
         | 
| 44 | 
            +
                  multiple_dropdowns_question: MultipleDropdownsQuestion,
         | 
| 45 | 
            +
                  matching_question: MatchingQuestion,
         | 
| 46 | 
            +
                  essay_question: EssayQuestion,
         | 
| 47 | 
            +
                  file_upload_question: FileUploadQuestion,
         | 
| 48 | 
            +
                  text_only_question: TextOnlyQuestion,
         | 
| 49 | 
            +
                  numerical_question: NumericalQuestion,
         | 
| 50 | 
            +
                  calculated_question: CalculatedQuestion,
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  "cc.multiple_choice.v0p1": MultipleChoiceQuestion,
         | 
| 53 | 
            +
                  "cc.multiple_response.v0p1": MultipleAnswersQuestion,
         | 
| 54 | 
            +
                  "cc.fib.v0p1": ShortAnswerQuestion,
         | 
| 55 | 
            +
                  "cc.true_false.v0p1": MultipleChoiceQuestion,
         | 
| 56 | 
            +
                  "cc.essay.v0p1": EssayQuestion,
         | 
| 57 | 
            +
                }
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                attr_accessor :items, :widgets, :item_banks, :assessments, :assets, :errors
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def initialize
         | 
| 62 | 
            +
                  @items = []
         | 
| 63 | 
            +
                  @widgets = []
         | 
| 64 | 
            +
                  @item_banks = []
         | 
| 65 | 
            +
                  @assessments = []
         | 
| 66 | 
            +
                  @assets = {}
         | 
| 67 | 
            +
                  @errors = {}
         | 
| 68 | 
            +
                  @namespace = SecureRandom.uuid
         | 
| 52 69 | 
             
                end
         | 
| 53 | 
            -
              end
         | 
| 54 70 |  | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 71 | 
            +
                class CanvasQuestionTypeNotSupportedError < RuntimeError
         | 
| 72 | 
            +
                  attr_reader :question_type
         | 
| 73 | 
            +
                  def initialize(question_type)
         | 
| 74 | 
            +
                    @question_type = question_type.to_s
         | 
| 75 | 
            +
                    super("Unsupported question type #{@question_type}")
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                end
         | 
| 58 78 |  | 
| 59 | 
            -
                 | 
| 60 | 
            -
                   | 
| 79 | 
            +
                class CanvasEntryTypeNotSupportedError < RuntimeError
         | 
| 80 | 
            +
                  attr_reader :question_type
         | 
| 81 | 
            +
                  def initialize(entry_type)
         | 
| 82 | 
            +
                    @entry_type = entry_type.to_s
         | 
| 83 | 
            +
                    super("Unsupported entry type #{@entry_type}")
         | 
| 84 | 
            +
                  end
         | 
| 61 85 | 
             
                end
         | 
| 62 | 
            -
              end
         | 
| 63 86 |  | 
| 87 | 
            +
                class CanvasQtiQuiz
         | 
| 88 | 
            +
                  extend Forwardable
         | 
| 89 | 
            +
                  def_delegators :@xml, :css, :at_css
         | 
| 64 90 |  | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
                file.close
         | 
| 70 | 
            -
              # Do we need to unlink?
         | 
| 71 | 
            -
              end
         | 
| 91 | 
            +
                  def initialize(qti_string:)
         | 
| 92 | 
            +
                    @xml = Nokogiri.XML(qti_string, &:noblanks)
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                end
         | 
| 72 95 |  | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 96 | 
            +
                def build_quiz_from_qti_string(qti_string)
         | 
| 97 | 
            +
                  CanvasQtiQuiz.new(qti_string: qti_string)
         | 
| 98 | 
            +
                end
         | 
| 76 99 |  | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 100 | 
            +
                def build_quiz_from_file(path)
         | 
| 101 | 
            +
                  qti_file = File.new path
         | 
| 102 | 
            +
                  qti_string = qti_file.read
         | 
| 103 | 
            +
                  CanvasQtiQuiz.new(qti_string: qti_string)
         | 
| 104 | 
            +
                ensure
         | 
| 105 | 
            +
                  qti_file.close
         | 
| 106 | 
            +
                end
         | 
| 84 107 |  | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 108 | 
            +
                def extract_type(xml)
         | 
| 109 | 
            +
                  xml.css(%{ item > itemmetadata > qtimetadata >
         | 
| 110 | 
            +
                    qtimetadatafield > fieldlabel:contains("question_type")})
         | 
| 111 | 
            +
                    &.first&.next&.text&.to_sym ||
         | 
| 112 | 
            +
                    xml.css(%{ item > itemmetadata > qtimetadata >
         | 
| 113 | 
            +
                      qtimetadatafield > fieldlabel:contains("cc_profile")})
         | 
| 114 | 
            +
                      &.first&.next&.text&.to_sym
         | 
| 90 115 | 
             
                end
         | 
| 91 | 
            -
              end
         | 
| 92 116 |  | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
                   | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 117 | 
            +
                def convert_item(qti_string:)
         | 
| 118 | 
            +
                  xml = Nokogiri.XML(qti_string, &:noblanks)
         | 
| 119 | 
            +
                  type = extract_type(xml)
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  if FEATURE_TYPES.include?(type)
         | 
| 122 | 
            +
                    learnosity_type = "feature"
         | 
| 123 | 
            +
                  else
         | 
| 124 | 
            +
                    learnosity_type = "question"
         | 
| 125 | 
            +
                  end
         | 
| 98 126 |  | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 101 | 
            -
             | 
| 127 | 
            +
                  question_class = TYPE_MAP[type]
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                  if question_class
         | 
| 130 | 
            +
                    question = question_class.new(xml)
         | 
| 131 | 
            +
                  else
         | 
| 132 | 
            +
                    raise CanvasQuestionTypeNotSupportedError.new(type)
         | 
| 133 | 
            +
                  end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                  [learnosity_type, question]
         | 
| 136 | 
            +
                end
         | 
| 102 137 |  | 
| 103 | 
            -
                 | 
| 104 | 
            -
                   | 
| 105 | 
            -
                else
         | 
| 106 | 
            -
                  learnosity_type = "question"
         | 
| 138 | 
            +
                def clean_title(title)
         | 
| 139 | 
            +
                  title&.gsub(/["']/, "")
         | 
| 107 140 | 
             
                end
         | 
| 108 141 |  | 
| 109 | 
            -
                 | 
| 142 | 
            +
                def convert_assessment(qti, path)
         | 
| 143 | 
            +
                  quiz = CanvasQtiQuiz.new(qti_string: qti)
         | 
| 144 | 
            +
                  assessment = quiz.at_css("assessment")
         | 
| 145 | 
            +
                  return nil unless assessment
         | 
| 110 146 |  | 
| 111 | 
            -
             | 
| 112 | 
            -
                   | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 147 | 
            +
                  ident = assessment.attribute("ident")&.value
         | 
| 148 | 
            +
                  reference = build_reference(ident)
         | 
| 149 | 
            +
                  title = clean_title(assessment.attribute("title").value)
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  item_refs = convert_items(quiz, path)
         | 
| 152 | 
            +
                  @assessments <<
         | 
| 153 | 
            +
                    {
         | 
| 154 | 
            +
                      reference:,
         | 
| 155 | 
            +
                      title:,
         | 
| 156 | 
            +
                      data: {
         | 
| 157 | 
            +
                        items: item_refs.uniq.map { |ref| { reference: ref } },
         | 
| 158 | 
            +
                        config: { title: },
         | 
| 159 | 
            +
                      },
         | 
| 160 | 
            +
                      status: "published",
         | 
| 161 | 
            +
                      tags: {},
         | 
| 162 | 
            +
                    }
         | 
| 115 163 | 
             
                end
         | 
| 116 164 |  | 
| 117 | 
            -
                 | 
| 118 | 
            -
             | 
| 165 | 
            +
                def convert_item_bank(qti_string, path)
         | 
| 166 | 
            +
                  qti = CanvasQtiQuiz.new(qti_string:)
         | 
| 167 | 
            +
                  item_bank = qti.at_css("objectbank")
         | 
| 168 | 
            +
                  return nil unless item_bank
         | 
| 119 169 |  | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 170 | 
            +
                  ident = item_bank.attribute("ident")&.value
         | 
| 171 | 
            +
                  title = clean_title(qti.css(%{ objectbank > qtimetadata >
         | 
| 172 | 
            +
                    qtimetadatafield > fieldlabel:contains("bank_title")})
         | 
| 173 | 
            +
                    &.first&.next&.text || '')
         | 
| 123 174 |  | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 130 | 
            -
             | 
| 131 | 
            -
             | 
| 132 | 
            -
             | 
| 133 | 
            -
                quiz.css("item").each.with_index do |item, index|
         | 
| 134 | 
            -
                  begin
         | 
| 135 | 
            -
                    item_title = item.attribute("title").value
         | 
| 136 | 
            -
                    learnosity_type, quiz_item = convert_item(qti_string: item.to_html)
         | 
| 137 | 
            -
             | 
| 138 | 
            -
                    item = {
         | 
| 139 | 
            -
                      title: item_title,
         | 
| 140 | 
            -
                      type: learnosity_type,
         | 
| 141 | 
            -
                      data: quiz_item.to_learnosity,
         | 
| 142 | 
            -
                      dynamic_content_data: quiz_item.dynamic_content_data()
         | 
| 175 | 
            +
                  meta = {
         | 
| 176 | 
            +
                    original_item_bank_ref: ident,
         | 
| 177 | 
            +
                  }
         | 
| 178 | 
            +
                  item_refs = convert_items(qti, path, meta:, tags: { "Item Bank" => [title] })
         | 
| 179 | 
            +
                  @item_banks <<
         | 
| 180 | 
            +
                    {
         | 
| 181 | 
            +
                      title: title,
         | 
| 182 | 
            +
                      ident: ident,
         | 
| 183 | 
            +
                      item_refs: item_refs,
         | 
| 143 184 | 
             
                    }
         | 
| 185 | 
            +
                end
         | 
| 144 186 |  | 
| 145 | 
            -
             | 
| 146 | 
            -
             | 
| 147 | 
            -
             | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
                    errors[ident].push({
         | 
| 151 | 
            -
                      index: index,
         | 
| 152 | 
            -
                      error_type: "unsupported_question",
         | 
| 153 | 
            -
                      question_type: e.question_type.to_s,
         | 
| 154 | 
            -
                      message: e.message,
         | 
| 155 | 
            -
                    })
         | 
| 156 | 
            -
                  rescue StandardError => e
         | 
| 157 | 
            -
                    errors[ident].push({
         | 
| 158 | 
            -
                      index: index,
         | 
| 159 | 
            -
                      error_type: e.class.to_s,
         | 
| 160 | 
            -
                      message: e.message,
         | 
| 161 | 
            -
                    })
         | 
| 187 | 
            +
                def build_reference(ident = nil)
         | 
| 188 | 
            +
                  if ident.present?
         | 
| 189 | 
            +
                    Digest::UUID.uuid_v5(@namespace, ident)
         | 
| 190 | 
            +
                  else
         | 
| 191 | 
            +
                    SecureRandom.uuid
         | 
| 162 192 | 
             
                  end
         | 
| 163 193 | 
             
                end
         | 
| 164 194 |  | 
| 165 | 
            -
                {
         | 
| 166 | 
            -
                   | 
| 167 | 
            -
                   | 
| 168 | 
            -
             | 
| 169 | 
            -
             | 
| 170 | 
            -
             | 
| 195 | 
            +
                def convert_items(qti, path, meta: {}, tags: {})
         | 
| 196 | 
            +
                  converted_item_refs = []
         | 
| 197 | 
            +
                  qti.css("item,bankentry_item,section").each.with_index do |item, index|
         | 
| 198 | 
            +
                    begin
         | 
| 199 | 
            +
                      ident = item.attribute("ident")&.value
         | 
| 200 | 
            +
                      if item.name == "section"
         | 
| 201 | 
            +
                        next if ident == "root_section"
         | 
| 171 202 |  | 
| 172 | 
            -
             | 
| 173 | 
            -
             | 
| 174 | 
            -
             | 
| 175 | 
            -
             | 
| 176 | 
            -
             | 
| 177 | 
            -
             | 
| 178 | 
            -
             | 
| 179 | 
            -
             | 
| 203 | 
            +
                        item.css("sourcebank_ref").each do |sourcebank_ref|
         | 
| 204 | 
            +
                          item_refs = @items.select { |i| i.dig(:metadata, :original_item_bank_ref) == sourcebank_ref.text }.map { |i| i[:reference] }
         | 
| 205 | 
            +
                          converted_item_refs += item_refs
         | 
| 206 | 
            +
                        end
         | 
| 207 | 
            +
                      elsif item.name == "bankentry_item"
         | 
| 208 | 
            +
                        item_ref = item.attribute("item_ref")&.value
         | 
| 209 | 
            +
                        if item_ref
         | 
| 210 | 
            +
                          converted_item_refs.push(build_reference(item_ref))
         | 
| 211 | 
            +
                        end
         | 
| 212 | 
            +
                      elsif item.name == "item"
         | 
| 213 | 
            +
                        reference = build_reference(ident)
         | 
| 214 | 
            +
                        item_title = item.attribute("title")&.value || ''
         | 
| 215 | 
            +
                        learnosity_type, quiz_item = convert_item(qti_string: item.to_html)
         | 
| 180 216 |  | 
| 181 | 
            -
             | 
| 182 | 
            -
             | 
| 183 | 
            -
             | 
| 184 | 
            -
             | 
| 217 | 
            +
                        item_widgets = [
         | 
| 218 | 
            +
                          {
         | 
| 219 | 
            +
                            type: learnosity_type,
         | 
| 220 | 
            +
                            data: quiz_item.convert(@assets, path),
         | 
| 221 | 
            +
                            reference: build_reference,
         | 
| 222 | 
            +
                          }
         | 
| 223 | 
            +
                        ]
         | 
| 224 | 
            +
                        @widgets += item_widgets
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                        @items << {
         | 
| 227 | 
            +
                          title: item_title,
         | 
| 228 | 
            +
                          reference:,
         | 
| 229 | 
            +
                          metadata: meta.merge({ original_item_ref: ident }),
         | 
| 230 | 
            +
                          definition: {
         | 
| 231 | 
            +
                            widgets: item_widgets.map{ |w| { reference: w[:reference] } },
         | 
| 232 | 
            +
                          },
         | 
| 233 | 
            +
                          questions: item_widgets.select{ |w| w[:type] == "question" }.map{ |w| w[:reference] },
         | 
| 234 | 
            +
                          features: item_widgets.select{ |w| w[:type] == "feature" }.map{ |w| w[:reference] },
         | 
| 235 | 
            +
                          status: "published",
         | 
| 236 | 
            +
                          tags: tags,
         | 
| 237 | 
            +
                          type: learnosity_type,
         | 
| 238 | 
            +
                          dynamic_content_data: quiz_item.dynamic_content_data()
         | 
| 239 | 
            +
                        }
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                        converted_item_refs.push(reference)
         | 
| 242 | 
            +
                      end
         | 
| 185 243 |  | 
| 186 | 
            -
             | 
| 187 | 
            -
             | 
| 188 | 
            -
             | 
| 189 | 
            -
             | 
| 190 | 
            -
             | 
| 191 | 
            -
             | 
| 192 | 
            -
             | 
| 193 | 
            -
             | 
| 194 | 
            -
             | 
| 195 | 
            -
             | 
| 196 | 
            -
             | 
| 197 | 
            -
             | 
| 244 | 
            +
                    rescue CanvasQuestionTypeNotSupportedError => e
         | 
| 245 | 
            +
                      @errors[ident] ||= []
         | 
| 246 | 
            +
                      @errors[ident].push({
         | 
| 247 | 
            +
                        index: index,
         | 
| 248 | 
            +
                        error_type: "unsupported_question",
         | 
| 249 | 
            +
                        question_type: e.question_type.to_s,
         | 
| 250 | 
            +
                        message: e.message,
         | 
| 251 | 
            +
                      })
         | 
| 252 | 
            +
                    rescue StandardError => e
         | 
| 253 | 
            +
                      @errors[ident] ||= []
         | 
| 254 | 
            +
                      @errors[ident].push({
         | 
| 255 | 
            +
                        index: index,
         | 
| 256 | 
            +
                        error_type: e.class.to_s,
         | 
| 257 | 
            +
                        message: e.message,
         | 
| 258 | 
            +
                      })
         | 
| 259 | 
            +
                    end
         | 
| 198 260 | 
             
                  end
         | 
| 261 | 
            +
                  converted_item_refs
         | 
| 262 | 
            +
                end
         | 
| 199 263 |  | 
| 200 | 
            -
             | 
| 201 | 
            -
             | 
| 202 | 
            -
             | 
| 203 | 
            -
             | 
| 204 | 
            -
             | 
| 264 | 
            +
                def convert_qti_file(path)
         | 
| 265 | 
            +
                  file = File.new(path)
         | 
| 266 | 
            +
                  qti_string = file.read
         | 
| 267 | 
            +
                  convert(qti_string)
         | 
| 268 | 
            +
                ensure
         | 
| 269 | 
            +
                  file.close
         | 
| 270 | 
            +
                  file.unlink
         | 
| 271 | 
            +
                end
         | 
| 272 | 
            +
             | 
| 273 | 
            +
                def imscc_quiz_paths(parsed_manifest)
         | 
| 274 | 
            +
                  resources = parsed_manifest.css("resources > resource[type^='imsqti_xmlv1p2']")
         | 
| 275 | 
            +
                  resources.map do |entry|
         | 
| 276 | 
            +
                    resource_path(parsed_manifest, entry)
         | 
| 277 | 
            +
                  end
         | 
| 278 | 
            +
                end
         | 
| 279 | 
            +
             | 
| 280 | 
            +
                def imscc_item_bank_paths(parsed_manifest)
         | 
| 281 | 
            +
                  resources = parsed_manifest.css("resources > resource[type='associatedcontent/imscc_xmlv1p1/learning-application-resource']")
         | 
| 282 | 
            +
                  resources.map do |entry|
         | 
| 283 | 
            +
                    resource_path(parsed_manifest, entry)
         | 
| 284 | 
            +
                  end
         | 
| 285 | 
            +
                end
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                def resource_path(parsed_manifest, entry)
         | 
| 288 | 
            +
                  # Use the Canvas non_cc_assignment qti path when possible.  This works for both classic and new quizzes
         | 
| 289 | 
            +
                  entry.css("dependency").each do |dependency|
         | 
| 290 | 
            +
                    ref = dependency.attribute("identifierref").value
         | 
| 291 | 
            +
                    parsed_manifest.css(%{resources > resource[identifier="#{ref}"] > file}).each do |file|
         | 
| 292 | 
            +
                      path = file.attribute("href").value
         | 
| 293 | 
            +
                      return path if path.match?(/^non_cc_assessments/)
         | 
| 294 | 
            +
                    end
         | 
| 295 | 
            +
                  end
         | 
| 296 | 
            +
                  entry.css("file").first&.attribute("href")&.value
         | 
| 297 | 
            +
                end
         | 
| 298 | 
            +
             | 
| 299 | 
            +
                def convert_imscc_export(path)
         | 
| 300 | 
            +
                  Zip::File.open(path) do |zip_file|
         | 
| 301 | 
            +
                    entry = zip_file.find_entry("imsmanifest.xml")
         | 
| 302 | 
            +
                    manifest = entry.get_input_stream.read
         | 
| 303 | 
            +
                    parsed_manifest = Nokogiri.XML(manifest, &:noblanks)
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                    item_bank_paths = imscc_item_bank_paths(parsed_manifest)
         | 
| 306 | 
            +
                    item_bank_paths.each do |item_bank_path|
         | 
| 307 | 
            +
                      qti = zip_file.find_entry(item_bank_path).get_input_stream.read
         | 
| 308 | 
            +
                      convert_item_bank(qti, File.dirname(item_bank_path))
         | 
| 309 | 
            +
                    end
         | 
| 310 | 
            +
             | 
| 311 | 
            +
                    assessment_paths = imscc_quiz_paths(parsed_manifest)
         | 
| 312 | 
            +
                    assessment_paths.each do |qti_path|
         | 
| 313 | 
            +
                      qti = zip_file.find_entry(qti_path).get_input_stream.read
         | 
| 314 | 
            +
                      convert_assessment(qti, File.dirname(qti_path))
         | 
| 315 | 
            +
                    end
         | 
| 316 | 
            +
             | 
| 317 | 
            +
                    {
         | 
| 318 | 
            +
                      errors: @errors,
         | 
| 319 | 
            +
                    }
         | 
| 320 | 
            +
                  end
         | 
| 321 | 
            +
                end
         | 
| 322 | 
            +
             | 
| 323 | 
            +
                def generate_learnosity_export(input_path, output_path)
         | 
| 324 | 
            +
                  result = convert_imscc_export(input_path)
         | 
| 325 | 
            +
             | 
| 326 | 
            +
                  export_writer = ExportWriter.new(output_path)
         | 
| 327 | 
            +
                  export_writer.write_to_zip("export.json", { version: 2.0 })
         | 
| 328 | 
            +
             | 
| 329 | 
            +
                  @assessments.each do |activity|
         | 
| 330 | 
            +
                    export_writer.write_to_zip("activities/#{activity[:reference]}.json", activity)
         | 
| 331 | 
            +
                  end
         | 
| 332 | 
            +
                  @items.each do |item|
         | 
| 333 | 
            +
                    export_writer.write_to_zip("items/#{item[:reference]}.json", item)
         | 
| 334 | 
            +
                  end
         | 
| 335 | 
            +
                  @widgets.each do |widget|
         | 
| 336 | 
            +
                    export_writer.write_to_zip("#{widget[:type]}s/#{widget[:reference]}.json", widget)
         | 
| 337 | 
            +
                  end
         | 
| 338 | 
            +
             | 
| 339 | 
            +
                  Zip::File.open(input_path) do |input|
         | 
| 340 | 
            +
                    @assets.each do |source, destination|
         | 
| 341 | 
            +
                      source = source.gsub(/^\//, '')
         | 
| 342 | 
            +
                      asset = input.find_entry(source) || input.find_entry("web_resources/#{source}")
         | 
| 343 | 
            +
                      if asset
         | 
| 344 | 
            +
                        export_writer.write_asset_to_zip("assets/#{destination}", input.read(asset))
         | 
| 345 | 
            +
                      end
         | 
| 346 | 
            +
                    end
         | 
| 347 | 
            +
                  end
         | 
| 348 | 
            +
                  export_writer.close
         | 
| 349 | 
            +
             | 
| 350 | 
            +
                  result
         | 
| 205 351 | 
             
                end
         | 
| 206 352 | 
             
              end
         | 
| 207 353 | 
             
            end
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            class ExportWriter
         | 
| 2 | 
            +
              def initialize(temp_file)
         | 
| 3 | 
            +
                @zip = Zip::File.open(temp_file.path, Zip::File::CREATE)
         | 
| 4 | 
            +
              end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def close
         | 
| 7 | 
            +
                @zip.close
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def write_to_zip(filename, content)
         | 
| 11 | 
            +
                @zip.get_output_stream(filename) do |file|
         | 
| 12 | 
            +
                  file << content.to_json
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              def write_asset_to_zip(filename, content)
         | 
| 17 | 
            +
                @zip.get_output_stream(filename) do |file|
         | 
| 18 | 
            +
                  file << content
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
            end
         | 
| @@ -37,13 +37,13 @@ module CanvasQtiToLearnosityConverter | |
| 37 37 | 
             
                  }
         | 
| 38 38 | 
             
                end
         | 
| 39 39 |  | 
| 40 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 41 | 
            -
                   | 
| 42 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 40 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 41 | 
            +
                  process_assets!(
         | 
| 43 42 | 
             
                    assets,
         | 
| 44 | 
            -
                    path | 
| 43 | 
            +
                    path,
         | 
| 45 44 | 
             
                    learnosity[:stimulus]
         | 
| 46 45 | 
             
                  )
         | 
| 46 | 
            +
                  learnosity
         | 
| 47 47 | 
             
                end
         | 
| 48 48 |  | 
| 49 49 | 
             
                def dynamic_content_data()
         | 
| @@ -12,13 +12,13 @@ module CanvasQtiToLearnosityConverter | |
| 12 12 | 
             
                  }
         | 
| 13 13 | 
             
                end
         | 
| 14 14 |  | 
| 15 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 16 | 
            -
                   | 
| 17 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 15 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 16 | 
            +
                  process_assets!(
         | 
| 18 17 | 
             
                    assets,
         | 
| 19 | 
            -
                    path | 
| 18 | 
            +
                    path,
         | 
| 20 19 | 
             
                    learnosity[:stimulus]
         | 
| 21 20 | 
             
                  )
         | 
| 21 | 
            +
                  learnosity
         | 
| 22 22 | 
             
                end
         | 
| 23 23 | 
             
              end
         | 
| 24 24 | 
             
            end
         | 
| @@ -25,13 +25,13 @@ module CanvasQtiToLearnosityConverter | |
| 25 25 | 
             
                  }
         | 
| 26 26 | 
             
                end
         | 
| 27 27 |  | 
| 28 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 29 | 
            -
                   | 
| 30 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 28 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 29 | 
            +
                  process_assets!(
         | 
| 31 30 | 
             
                    assets,
         | 
| 32 | 
            -
                    path | 
| 31 | 
            +
                    path,
         | 
| 33 32 | 
             
                    learnosity[:stimulus]
         | 
| 34 33 | 
             
                  )
         | 
| 34 | 
            +
                  learnosity
         | 
| 35 35 | 
             
                end
         | 
| 36 36 | 
             
              end
         | 
| 37 37 | 
             
            end
         | 
| @@ -11,14 +11,13 @@ module CanvasQtiToLearnosityConverter | |
| 11 11 | 
             
                  }
         | 
| 12 12 | 
             
                end
         | 
| 13 13 |  | 
| 14 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 15 | 
            -
                   | 
| 16 | 
            -
             | 
| 17 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 14 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 15 | 
            +
                  process_assets!(
         | 
| 18 16 | 
             
                    assets,
         | 
| 19 | 
            -
                    path | 
| 17 | 
            +
                    path,
         | 
| 20 18 | 
             
                    learnosity[:template]
         | 
| 21 19 | 
             
                  )
         | 
| 20 | 
            +
                  learnosity
         | 
| 22 21 | 
             
                end
         | 
| 23 22 |  | 
| 24 23 | 
             
                def extract_validation()
         | 
| @@ -66,13 +66,13 @@ module CanvasQtiToLearnosityConverter | |
| 66 66 | 
             
                  end
         | 
| 67 67 | 
             
                end
         | 
| 68 68 |  | 
| 69 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 70 | 
            -
                   | 
| 71 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 69 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 70 | 
            +
                  process_assets!(
         | 
| 72 71 | 
             
                    assets,
         | 
| 73 | 
            -
                    path | 
| 72 | 
            +
                    path,
         | 
| 74 73 | 
             
                    learnosity[:stimulus]
         | 
| 75 74 | 
             
                  )
         | 
| 75 | 
            +
                  learnosity
         | 
| 76 76 | 
             
                end
         | 
| 77 77 | 
             
              end
         | 
| 78 78 | 
             
            end
         | 
| @@ -46,21 +46,21 @@ module CanvasQtiToLearnosityConverter | |
| 46 46 | 
             
                  }
         | 
| 47 47 | 
             
                end
         | 
| 48 48 |  | 
| 49 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 50 | 
            -
                   | 
| 51 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 49 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 50 | 
            +
                  process_assets!(
         | 
| 52 51 | 
             
                    assets,
         | 
| 53 | 
            -
                    path | 
| 52 | 
            +
                    path,
         | 
| 54 53 | 
             
                    learnosity[:stimulus]
         | 
| 55 54 | 
             
                  )
         | 
| 56 55 |  | 
| 57 56 | 
             
                  learnosity[:options].each.with_index do |option, index|
         | 
| 58 | 
            -
                     | 
| 57 | 
            +
                    process_assets!(
         | 
| 59 58 | 
             
                      assets,
         | 
| 60 | 
            -
                      path | 
| 59 | 
            +
                      path,
         | 
| 61 60 | 
             
                      option["label"]
         | 
| 62 61 | 
             
                    )
         | 
| 63 62 | 
             
                  end
         | 
| 63 | 
            +
                  learnosity
         | 
| 64 64 | 
             
                end
         | 
| 65 65 | 
             
              end
         | 
| 66 66 |  | 
| @@ -53,13 +53,13 @@ module CanvasQtiToLearnosityConverter | |
| 53 53 | 
             
                  }
         | 
| 54 54 | 
             
                end
         | 
| 55 55 |  | 
| 56 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 57 | 
            -
                   | 
| 58 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 56 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 57 | 
            +
                  process_assets!(
         | 
| 59 58 | 
             
                    assets,
         | 
| 60 | 
            -
                    path | 
| 59 | 
            +
                    path,
         | 
| 61 60 | 
             
                    learnosity[:stimulus]
         | 
| 62 61 | 
             
                  )
         | 
| 62 | 
            +
                  learnosity
         | 
| 63 63 | 
             
                end
         | 
| 64 64 | 
             
              end
         | 
| 65 65 | 
             
            end
         | 
| @@ -31,5 +31,34 @@ module CanvasQtiToLearnosityConverter | |
| 31 31 | 
             
                def dynamic_content_data()
         | 
| 32 32 | 
             
                  {}
         | 
| 33 33 | 
             
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def process_assets!(assets, path, text)
         | 
| 36 | 
            +
                  doc = Nokogiri::XML.fragment(text)
         | 
| 37 | 
            +
                  changed = false
         | 
| 38 | 
            +
                  doc.css("img").each do |node|
         | 
| 39 | 
            +
                    source = node["src"]
         | 
| 40 | 
            +
                    next if !source
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    source = URI::DEFAULT_PARSER.unescape(source)
         | 
| 43 | 
            +
                    if /^\$IMS-CC-FILEBASE\$(.*)/.match(source) || /^((?!https?:).*)/.match(source)
         | 
| 44 | 
            +
                      if source.start_with?("$IMS-CC-FILEBASE$")
         | 
| 45 | 
            +
                        path = ''
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                      asset_path = $1
         | 
| 48 | 
            +
                      asset_path = asset_path.split("?").first.gsub(/^\//, '')
         | 
| 49 | 
            +
                      asset_path = File.join(path, asset_path)
         | 
| 50 | 
            +
                      clean_ext = File.extname(asset_path).gsub(/[^a-z0-9_.-]/i, '')
         | 
| 51 | 
            +
                      assets[asset_path] ||= "#{SecureRandom.uuid}#{clean_ext}"
         | 
| 52 | 
            +
                      node["src"] = "___EXPORT_ROOT___/assets/#{assets[asset_path]}"
         | 
| 53 | 
            +
                      changed = true
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                  text.replace(doc.to_xml) if changed
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def convert(assets, path)
         | 
| 60 | 
            +
                  object = to_learnosity
         | 
| 61 | 
            +
                  add_learnosity_assets(assets, path, object)
         | 
| 62 | 
            +
                end
         | 
| 34 63 | 
             
              end
         | 
| 35 64 | 
             
            end
         | 
| @@ -28,13 +28,13 @@ module CanvasQtiToLearnosityConverter | |
| 28 28 | 
             
                  }
         | 
| 29 29 | 
             
                end
         | 
| 30 30 |  | 
| 31 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 32 | 
            -
                   | 
| 33 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 31 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 32 | 
            +
                  process_assets!(
         | 
| 34 33 | 
             
                    assets,
         | 
| 35 | 
            -
                    path | 
| 34 | 
            +
                    path,
         | 
| 36 35 | 
             
                    learnosity[:stimulus]
         | 
| 37 36 | 
             
                  )
         | 
| 37 | 
            +
                  learnosity
         | 
| 38 38 | 
             
                end
         | 
| 39 39 | 
             
              end
         | 
| 40 40 | 
             
            end
         | 
| @@ -26,14 +26,13 @@ module CanvasQtiToLearnosityConverter | |
| 26 26 | 
             
                  extract_mattext(template_node_list.first)
         | 
| 27 27 | 
             
                end
         | 
| 28 28 |  | 
| 29 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 30 | 
            -
                   | 
| 31 | 
            -
             | 
| 32 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 29 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 30 | 
            +
                  process_assets!(
         | 
| 33 31 | 
             
                    assets,
         | 
| 34 | 
            -
                    path | 
| 32 | 
            +
                    path,
         | 
| 35 33 | 
             
                    learnosity[:template]
         | 
| 36 34 | 
             
                  )
         | 
| 35 | 
            +
                  learnosity
         | 
| 37 36 | 
             
                end
         | 
| 38 37 | 
             
              end
         | 
| 39 38 | 
             
            end
         | 
| @@ -10,13 +10,13 @@ module CanvasQtiToLearnosityConverter | |
| 10 10 | 
             
                  }
         | 
| 11 11 | 
             
                end
         | 
| 12 12 |  | 
| 13 | 
            -
                def add_learnosity_assets(assets, path)
         | 
| 14 | 
            -
                   | 
| 15 | 
            -
                  CanvasQtiToLearnosityConverter.add_files_to_assets(
         | 
| 13 | 
            +
                def add_learnosity_assets(assets, path, learnosity)
         | 
| 14 | 
            +
                  process_assets!(
         | 
| 16 15 | 
             
                    assets,
         | 
| 17 | 
            -
                    path | 
| 16 | 
            +
                    path,
         | 
| 18 17 | 
             
                    learnosity[:content]
         | 
| 19 18 | 
             
                  )
         | 
| 19 | 
            +
                  learnosity
         | 
| 20 20 | 
             
                end
         | 
| 21 21 | 
             
              end
         | 
| 22 22 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: canvas_qti_to_learnosity_converter
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version:  | 
| 4 | 
            +
              version: 3.0.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Atomic Jolt
         | 
| @@ -9,7 +9,7 @@ authors: | |
| 9 9 | 
             
            autorequire:
         | 
| 10 10 | 
             
            bindir: bin
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date:  | 
| 12 | 
            +
            date: 2024-08-27 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 15 | 
             
              name: bundler
         | 
| @@ -95,6 +95,20 @@ dependencies: | |
| 95 95 | 
             
                - - ">="
         | 
| 96 96 | 
             
                  - !ruby/object:Gem::Version
         | 
| 97 97 | 
             
                    version: '0'
         | 
| 98 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 99 | 
            +
              name: activesupport
         | 
| 100 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 101 | 
            +
                requirements:
         | 
| 102 | 
            +
                - - ">="
         | 
| 103 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 104 | 
            +
                    version: '0'
         | 
| 105 | 
            +
              type: :runtime
         | 
| 106 | 
            +
              prerelease: false
         | 
| 107 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 108 | 
            +
                requirements:
         | 
| 109 | 
            +
                - - ">="
         | 
| 110 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 111 | 
            +
                    version: '0'
         | 
| 98 112 | 
             
            description:
         | 
| 99 113 | 
             
            email:
         | 
| 100 114 | 
             
            - support@atomicjolt.com
         | 
| @@ -115,6 +129,7 @@ files: | |
| 115 129 | 
             
            - canvas_qti_to_learnosity_converter.gemspec
         | 
| 116 130 | 
             
            - lib/canvas_qti_to_learnosity_converter.rb
         | 
| 117 131 | 
             
            - lib/canvas_qti_to_learnosity_converter/convert.rb
         | 
| 132 | 
            +
            - lib/canvas_qti_to_learnosity_converter/export_writer.rb
         | 
| 118 133 | 
             
            - lib/canvas_qti_to_learnosity_converter/questions/calculated.rb
         | 
| 119 134 | 
             
            - lib/canvas_qti_to_learnosity_converter/questions/essay.rb
         | 
| 120 135 | 
             
            - lib/canvas_qti_to_learnosity_converter/questions/file_upload.rb
         |