qti 0.2.12 → 0.2.14
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/Rakefile +9 -1
- data/lib/qti/v1/models/assessment.rb +2 -2
- data/lib/qti/v1/models/assessment_item.rb +5 -29
- data/lib/qti/v1/models/interactions/base_interaction.rb +33 -0
- data/lib/qti/v1/models/interactions/choice_interaction.rb +44 -0
- data/lib/qti/v1/models/interactions/match_interaction.rb +60 -0
- data/lib/qti/v1/models/interactions/ordering_interaction.rb +23 -0
- data/lib/qti/v1/models/interactions/string_interaction.rb +1 -3
- data/lib/qti/v1/models/interactions.rb +1 -3
- data/lib/qti/v1/models/scoring_data.rb +7 -0
- data/lib/qti/v2/models/assessment_item.rb +2 -6
- data/lib/qti/v2/models/choices/simple_associable_choice.rb +12 -0
- data/lib/qti/v2/models/interactions/base_interaction.rb +43 -0
- data/lib/qti/v2/models/interactions/choice_interaction.rb +2 -12
- data/lib/qti/v2/models/interactions/extended_text_interaction.rb +1 -5
- data/lib/qti/v2/models/interactions/match_interaction.rb +52 -0
- data/lib/qti/v2/models/scoring_data.rb +7 -0
- data/lib/qti.rb +6 -0
- data/spec/fixtures/items_1.2/matching.xml +74 -0
- data/spec/fixtures/items_1.2/multiple_answer.xml +1 -1
- data/spec/fixtures/items_1.2/multiple_choice.xml +1 -1
- data/spec/fixtures/items_1.2/ordering.xml +20 -20
- data/spec/fixtures/items_1.2/true_false.xml +1 -1
- data/spec/fixtures/items_2.1/match.xml +33 -39
- data/spec/fixtures/test_qti_1.2/quiz.xml +209 -216
- data/spec/lib/qti/v1/models/assessment_item_spec.rb +20 -2
- data/spec/lib/qti/v1/models/interactions/{logical_identifier_interaction_spec.rb → choice_interaction_spec.rb} +1 -1
- data/spec/lib/qti/v2/models/assessment_item_spec.rb +24 -0
- metadata +15 -5
- data/lib/qti/v1/models/interactions/logical_identifier_interaction.rb +0 -36
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 7e3dbac8ff4eda0f1c6f67c6f8e7e1673ac8014b
         | 
| 4 | 
            +
              data.tar.gz: 807c9a6be3ea4e1887966bb5734015dc017efa04
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: a628c8aa5c49fdea190b112136ab3ad22861725437b7c167c2ac6188ae7970a5641a6d794243726d4b9c24c0986ad33900d22816a4576fa056e5acc50c328db8
         | 
| 7 | 
            +
              data.tar.gz: 1a92b8176ca835e95923c936ae5c86b77c60e9ba702c7f6dd0d84d082c1ae6aa6eb63a670681bbfb34bb54ada67391c2ab394f4d6182f631712bf57b5376e83f
         | 
    
        data/Rakefile
    CHANGED
    
    
| @@ -5,11 +5,11 @@ module Qti | |
| 5 5 | 
             
                module Models
         | 
| 6 6 | 
             
                  class Assessment < Qti::V1::Models::Base
         | 
| 7 7 | 
             
                    def title
         | 
| 8 | 
            -
                      @title ||= xpath_with_single_check(' | 
| 8 | 
            +
                      @title ||= xpath_with_single_check('.//xmlns:assessment/@title')&.content
         | 
| 9 9 | 
             
                    end
         | 
| 10 10 |  | 
| 11 11 | 
             
                    def assessment_items
         | 
| 12 | 
            -
                      @doc.xpath(' | 
| 12 | 
            +
                      @doc.xpath('.//xmlns:item')
         | 
| 13 13 | 
             
                    end
         | 
| 14 14 | 
             
                  end
         | 
| 15 15 | 
             
                end
         | 
| @@ -7,8 +7,6 @@ module Qti | |
| 7 7 | 
             
                  class AssessmentItem < Qti::V1::Models::Base
         | 
| 8 8 | 
             
                    attr_reader :doc
         | 
| 9 9 |  | 
| 10 | 
            -
                    ScoringData = Struct.new(:values, :rcardinality)
         | 
| 11 | 
            -
             | 
| 12 10 | 
             
                    def initialize(item)
         | 
| 13 11 | 
             
                      @doc = item
         | 
| 14 12 | 
             
                    end
         | 
| @@ -16,8 +14,8 @@ module Qti | |
| 16 14 | 
             
                    def item_body
         | 
| 17 15 | 
             
                      @item_body ||= begin
         | 
| 18 16 | 
             
                        node = @doc.dup
         | 
| 19 | 
            -
                        presentation = node.at_xpath(' | 
| 20 | 
            -
                        prompt = presentation.at_xpath(' | 
| 17 | 
            +
                        presentation = node.at_xpath('.//xmlns:presentation')
         | 
| 18 | 
            +
                        prompt = presentation.at_xpath('.//xmlns:mattext').content
         | 
| 21 19 | 
             
                        sanitize_content!(prompt)
         | 
| 22 20 | 
             
                      end
         | 
| 23 21 | 
             
                    end
         | 
| @@ -32,12 +30,12 @@ module Qti | |
| 32 30 |  | 
| 33 31 | 
             
                    def points_possible
         | 
| 34 32 | 
             
                      @points_possible ||= begin
         | 
| 35 | 
            -
                        @doc.at_xpath('.//decvar/@maxvalue')&.value || @doc.at_xpath('.//decvar/@defaultval')&.value
         | 
| 33 | 
            +
                        @doc.at_xpath('.//xmlns:decvar/@maxvalue')&.value || @doc.at_xpath('.//xmlns:decvar/@defaultval')&.value
         | 
| 36 34 | 
             
                      end
         | 
| 37 35 | 
             
                    end
         | 
| 38 36 |  | 
| 39 37 | 
             
                    def rcardinality
         | 
| 40 | 
            -
                      @rcardinality ||= @doc.at_xpath('.//response_lid/@rcardinality').value
         | 
| 38 | 
            +
                      @rcardinality ||= @doc.at_xpath('.//xmlns:response_lid/@rcardinality').value
         | 
| 41 39 | 
             
                    end
         | 
| 42 40 |  | 
| 43 41 | 
             
                    def interaction_model
         | 
| @@ -47,29 +45,7 @@ module Qti | |
| 47 45 | 
             
                    end
         | 
| 48 46 |  | 
| 49 47 | 
             
                    def scoring_data_structs
         | 
| 50 | 
            -
                      @scoring_data_structs ||=
         | 
| 51 | 
            -
                        if ordering?
         | 
| 52 | 
            -
                          scoring_data_structs_for_ordering
         | 
| 53 | 
            -
                        else
         | 
| 54 | 
            -
                          default_scoring_data_structs
         | 
| 55 | 
            -
                        end
         | 
| 56 | 
            -
                    end
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                    private
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                    def default_scoring_data_structs
         | 
| 61 | 
            -
                      choice_nodes = doc.xpath('.//respcondition')
         | 
| 62 | 
            -
                      choice_nodes.select { |choice_node| choice_node.at_xpath('.//setvar').content.to_f.positive? }
         | 
| 63 | 
            -
                                  .map { |value_node| ScoringData.new(value_node.at_xpath('.//varequal').content, rcardinality) }
         | 
| 64 | 
            -
                    end
         | 
| 65 | 
            -
             | 
| 66 | 
            -
                    def scoring_data_structs_for_ordering
         | 
| 67 | 
            -
                      correct_order = @doc.xpath('.//conditionvar/varequal').map{|node| node.content }
         | 
| 68 | 
            -
                      [ScoringData.new(correct_order, rcardinality)]
         | 
| 69 | 
            -
                    end
         | 
| 70 | 
            -
             | 
| 71 | 
            -
                    def ordering?
         | 
| 72 | 
            -
                      rcardinality == 'Ordered'
         | 
| 48 | 
            +
                      @scoring_data_structs ||= interaction_model.scoring_data_structs
         | 
| 73 49 | 
             
                    end
         | 
| 74 50 | 
             
                  end
         | 
| 75 51 | 
             
                end
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            module Qti
         | 
| 2 | 
            +
              module V1
         | 
| 3 | 
            +
                module Models
         | 
| 4 | 
            +
                  module Interactions
         | 
| 5 | 
            +
                    class BaseInteraction < Qti::V1::Models::Base
         | 
| 6 | 
            +
                      attr_reader :node
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                      def self.matches(node)
         | 
| 9 | 
            +
                        false
         | 
| 10 | 
            +
                      end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      def initialize(node)
         | 
| 13 | 
            +
                        @node = node
         | 
| 14 | 
            +
                      end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      def shuffled?
         | 
| 17 | 
            +
                        @node.at_xpath('.//xmlns:render_choice/@shuffle')&.value == 'Yes'
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      def scoring_data_structs
         | 
| 21 | 
            +
                        raise NotImplementedError
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      private
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      def rcardinality
         | 
| 27 | 
            +
                        @rcardinality ||= @node.at_xpath('.//xmlns:response_lid/@rcardinality').value
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            module Qti
         | 
| 2 | 
            +
              module V1
         | 
| 3 | 
            +
                module Models
         | 
| 4 | 
            +
                  module Interactions
         | 
| 5 | 
            +
                    class ChoiceInteraction < BaseInteraction
         | 
| 6 | 
            +
                      # This will know if a class matches
         | 
| 7 | 
            +
                      def self.matches(node)
         | 
| 8 | 
            +
                        matches = node.xpath('.//xmlns:response_lid')
         | 
| 9 | 
            +
                        return false if matches.count > 1  || matches.empty?
         | 
| 10 | 
            +
                        rcardinality = matches.first.attributes['rcardinality']&.value || 'Single'
         | 
| 11 | 
            +
                        return false if rcardinality == 'Ordered'
         | 
| 12 | 
            +
                        new(node)
         | 
| 13 | 
            +
                      end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                      def initialize(node)
         | 
| 16 | 
            +
                        @node = node
         | 
| 17 | 
            +
                      end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                      def shuffled?
         | 
| 20 | 
            +
                        node.at_xpath('.//xmlns:render_choice/@shuffle')&.value == 'Yes'
         | 
| 21 | 
            +
                      end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      def answers
         | 
| 24 | 
            +
                        @answers ||= answer_nodes.map do |node|
         | 
| 25 | 
            +
                          V1::Models::Choices::LogicalIdentifierChoice.new(node)
         | 
| 26 | 
            +
                        end
         | 
| 27 | 
            +
                      end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                      def scoring_data_structs
         | 
| 30 | 
            +
                        choice_nodes = node.xpath('.//xmlns:respcondition')
         | 
| 31 | 
            +
                        set_var_nodes = choice_nodes.select { |choice_node| choice_node.at_xpath('.//xmlns:setvar').content.to_f.positive? }
         | 
| 32 | 
            +
                        set_var_nodes.map { |value_node| ScoringData.new(value_node.at_xpath('.//xmlns:varequal').content, rcardinality) }
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      private
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      def answer_nodes
         | 
| 38 | 
            +
                        node.xpath('.//xmlns:response_label')
         | 
| 39 | 
            +
                      end
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,60 @@ | |
| 1 | 
            +
            module Qti
         | 
| 2 | 
            +
              module V1
         | 
| 3 | 
            +
                module Models
         | 
| 4 | 
            +
                  module Interactions
         | 
| 5 | 
            +
                    class MatchInteraction < BaseInteraction
         | 
| 6 | 
            +
                      attr_reader :node
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                      def self.matches(node)
         | 
| 9 | 
            +
                        matches = node.xpath('.//xmlns:response_lid')
         | 
| 10 | 
            +
                        return false if matches.count <= 1
         | 
| 11 | 
            +
                        new(node)
         | 
| 12 | 
            +
                      end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      def initialize(node)
         | 
| 15 | 
            +
                        @node = node
         | 
| 16 | 
            +
                      end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      def answers
         | 
| 19 | 
            +
                        @answers ||= answer_nodes.map do |node|
         | 
| 20 | 
            +
                          V1::Models::Choices::LogicalIdentifierChoice.new(node)
         | 
| 21 | 
            +
                        end
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      def questions
         | 
| 25 | 
            +
                        node.xpath('.//xmlns:response_lid').map do |lid_node|
         | 
| 26 | 
            +
                          question_body = lid_node.at_xpath('.//xmlns:mattext').text
         | 
| 27 | 
            +
                          { id: lid_node.attributes['ident'].value, question_body: question_body }
         | 
| 28 | 
            +
                        end
         | 
| 29 | 
            +
                      end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                      def scoring_data_structs
         | 
| 32 | 
            +
                        matches = node.xpath('.//xmlns:varequal').map do |node|
         | 
| 33 | 
            +
                          [node.attributes['respident'].value, answers_map[node.content]]
         | 
| 34 | 
            +
                        end
         | 
| 35 | 
            +
                        [Models::ScoringData.new(Hash[matches], rcardinality)]
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                      private
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                      def answers_map
         | 
| 41 | 
            +
                        @answers_map ||= answers.reduce({}) do |acc, answer|
         | 
| 42 | 
            +
                          acc.update answer.identifier => answer.item_body
         | 
| 43 | 
            +
                        end
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                      def answer_nodes
         | 
| 47 | 
            +
                        responses = []
         | 
| 48 | 
            +
                        response_ids = {}
         | 
| 49 | 
            +
                        node.xpath('.//xmlns:response_label').each do |answer_node|
         | 
| 50 | 
            +
                          ident = answer_node.attributes['ident'].value
         | 
| 51 | 
            +
                          responses << answer_node unless response_ids.key? ident
         | 
| 52 | 
            +
                          response_ids[ident] = 1
         | 
| 53 | 
            +
                        end
         | 
| 54 | 
            +
                        responses
         | 
| 55 | 
            +
                      end
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            module Qti
         | 
| 2 | 
            +
              module V1
         | 
| 3 | 
            +
                module Models
         | 
| 4 | 
            +
                  module Interactions
         | 
| 5 | 
            +
                    class OrderingInteraction < ChoiceInteraction
         | 
| 6 | 
            +
                      # This will know if a class matches
         | 
| 7 | 
            +
                      def self.matches(node)
         | 
| 8 | 
            +
                        matches = node.xpath('.//xmlns:response_lid')
         | 
| 9 | 
            +
                        return false if matches.count > 1  || matches.empty?
         | 
| 10 | 
            +
                        rcardinality = matches.first.attributes['rcardinality']&.value || 'Single'
         | 
| 11 | 
            +
                        return false if rcardinality != 'Ordered'
         | 
| 12 | 
            +
                        new(node)
         | 
| 13 | 
            +
                      end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                      def scoring_data_structs
         | 
| 16 | 
            +
                        correct_order = node.xpath('.//xmlns:varequal').map(&:content)
         | 
| 17 | 
            +
                        [ScoringData.new(correct_order, rcardinality)]
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -2,14 +2,12 @@ module Qti | |
| 2 2 | 
             
              module V1
         | 
| 3 3 | 
             
                module Models
         | 
| 4 4 | 
             
                  module Interactions
         | 
| 5 | 
            -
                    class StringInteraction <  | 
| 5 | 
            +
                    class StringInteraction < BaseInteraction
         | 
| 6 6 | 
             
                      @node_name = 'response_str'.freeze
         | 
| 7 7 | 
             
                      # This will know if a class matches
         | 
| 8 8 | 
             
                      def self.matches(node)
         | 
| 9 9 | 
             
                        matches = node.children.children.select { |n| n.name == @node_name }
         | 
| 10 10 | 
             
                        return false if matches.empty?
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                        raise Qti::UnsupportedSchema if matches.size > 1
         | 
| 13 11 | 
             
                        new(matches.first)
         | 
| 14 12 | 
             
                      end
         | 
| 15 13 |  | 
| @@ -39,16 +39,12 @@ module Qti | |
| 39 39 |  | 
| 40 40 | 
             
                    def interaction_model
         | 
| 41 41 | 
             
                      @interaction_model ||= begin
         | 
| 42 | 
            -
                        V2::Models::Interactions.interaction_model( | 
| 42 | 
            +
                        V2::Models::Interactions.interaction_model(@doc)
         | 
| 43 43 | 
             
                      end
         | 
| 44 44 | 
             
                    end
         | 
| 45 45 |  | 
| 46 46 | 
             
                    def scoring_data_structs
         | 
| 47 | 
            -
                      @scoring_data_structs ||=  | 
| 48 | 
            -
                        type = xpath_with_single_check('//xmlns:responseDeclaration/@baseType').content
         | 
| 49 | 
            -
                        value_nodes = @doc.xpath('//xmlns:responseDeclaration/xmlns:correctResponse/xmlns:value')
         | 
| 50 | 
            -
                        value_nodes.map { |value_node| ScoringData.new(value_node.content, type) }
         | 
| 51 | 
            -
                      end
         | 
| 47 | 
            +
                      @scoring_data_structs ||= interaction_model.scoring_data_structs
         | 
| 52 48 | 
             
                    end
         | 
| 53 49 |  | 
| 54 50 | 
             
                    private
         | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            module Qti
         | 
| 2 | 
            +
              module V2
         | 
| 3 | 
            +
                module Models
         | 
| 4 | 
            +
                  module Interactions
         | 
| 5 | 
            +
                    class BaseInteraction < Qti::V2::Models::Base
         | 
| 6 | 
            +
                      attr_reader :node
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                      def self.matches(node)
         | 
| 9 | 
            +
                        false
         | 
| 10 | 
            +
                      end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      def initialize(node)
         | 
| 13 | 
            +
                        @node = node
         | 
| 14 | 
            +
                      end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      def shuffled?
         | 
| 17 | 
            +
                        @node.at_xpath('.//render_choice/@shuffle')&.value == 'Yes'
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      def scoring_data_structs
         | 
| 21 | 
            +
                        @scoring_data_structs ||= begin
         | 
| 22 | 
            +
                          type = xpath_with_single_check('//xmlns:responseDeclaration/@baseType').content
         | 
| 23 | 
            +
                          value_nodes = node.xpath('//xmlns:responseDeclaration/xmlns:correctResponse/xmlns:value')
         | 
| 24 | 
            +
                          value_nodes.map { |value_node| Models::ScoringData.new(value_node.content, type) }
         | 
| 25 | 
            +
                        end
         | 
| 26 | 
            +
                      end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      def xpath_with_single_check(xpath)
         | 
| 29 | 
            +
                        node_list = node.xpath(xpath)
         | 
| 30 | 
            +
                        raise Qti::ParseError, 'Too many matches' if node_list.count > 1
         | 
| 31 | 
            +
                        node_list.first
         | 
| 32 | 
            +
                      end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      private
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                      def rcardinality
         | 
| 37 | 
            +
                        @rcardinality ||= @node.at_xpath('.//response_lid/@rcardinality').value
         | 
| 38 | 
            +
                      end
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
            end
         | 
| @@ -2,25 +2,15 @@ module Qti | |
| 2 2 | 
             
              module V2
         | 
| 3 3 | 
             
                module Models
         | 
| 4 4 | 
             
                  module Interactions
         | 
| 5 | 
            -
                    class ChoiceInteraction <  | 
| 6 | 
            -
                      @node_name = 'choiceInteraction'.freeze
         | 
| 7 | 
            -
                      # This will know if a class matches
         | 
| 5 | 
            +
                    class ChoiceInteraction < BaseInteraction
         | 
| 8 6 | 
             
                      def self.matches(node)
         | 
| 9 | 
            -
                        matches = node. | 
| 7 | 
            +
                        matches = node.xpath('.//xmlns:choiceInteraction')
         | 
| 10 8 | 
             
                        return false if matches.empty?
         | 
| 11 9 |  | 
| 12 10 | 
             
                        raise Qti::UnsupportedSchema if matches.size > 1
         | 
| 13 11 | 
             
                        new(matches.first)
         | 
| 14 12 | 
             
                      end
         | 
| 15 13 |  | 
| 16 | 
            -
                      def initialize(node)
         | 
| 17 | 
            -
                        @node = node
         | 
| 18 | 
            -
                      end
         | 
| 19 | 
            -
             | 
| 20 | 
            -
                      def shuffled?
         | 
| 21 | 
            -
                        @node.attributes['shuffle'].value == 'true'
         | 
| 22 | 
            -
                      end
         | 
| 23 | 
            -
             | 
| 24 14 | 
             
                      def answers
         | 
| 25 15 | 
             
                        @answers ||= answer_nodes.map do |node|
         | 
| 26 16 | 
             
                          V2::Models::Choices::SimpleChoice.new(node)
         | 
| @@ -2,7 +2,7 @@ module Qti | |
| 2 2 | 
             
              module V2
         | 
| 3 3 | 
             
                module Models
         | 
| 4 4 | 
             
                  module Interactions
         | 
| 5 | 
            -
                    class ExtendedTextInteraction <  | 
| 5 | 
            +
                    class ExtendedTextInteraction < BaseInteraction
         | 
| 6 6 | 
             
                      @node_name = 'extendedTextInteraction'.freeze
         | 
| 7 7 | 
             
                      # This will know if a class matches
         | 
| 8 8 | 
             
                      def self.matches(node)
         | 
| @@ -13,10 +13,6 @@ module Qti | |
| 13 13 | 
             
                        new(matches.first)
         | 
| 14 14 | 
             
                      end
         | 
| 15 15 |  | 
| 16 | 
            -
                      def initialize(node)
         | 
| 17 | 
            -
                        @node = node
         | 
| 18 | 
            -
                      end
         | 
| 19 | 
            -
             | 
| 20 16 | 
             
                      # not used yet
         | 
| 21 17 | 
             
                      def expected_lines
         | 
| 22 18 | 
             
                        @node.attributes['expectedLines']&.value&.to_i || 0
         | 
| @@ -0,0 +1,52 @@ | |
| 1 | 
            +
            module Qti
         | 
| 2 | 
            +
              module V2
         | 
| 3 | 
            +
                module Models
         | 
| 4 | 
            +
                  module Interactions
         | 
| 5 | 
            +
                    class MatchInteraction < BaseInteraction
         | 
| 6 | 
            +
                      def self.matches(node)
         | 
| 7 | 
            +
                        matches = node.xpath('//xmlns:matchInteraction')
         | 
| 8 | 
            +
                        return false if matches.empty?
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                        raise Qti::UnsupportedSchema if matches.size > 1
         | 
| 11 | 
            +
                        new(node)
         | 
| 12 | 
            +
                      end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      def questions
         | 
| 15 | 
            +
                        node.xpath('.//xmlns:correctResponse//xmlns:value')
         | 
| 16 | 
            +
                            .map { |value| value.content.split.first }
         | 
| 17 | 
            +
                            .map { |id| { id: id, question_body: choices_by_identifier[id].content } }
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      def answers
         | 
| 21 | 
            +
                        answer_nodes.map { |node| Choices::SimpleAssociableChoice.new(node) }
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      def scoring_data_structs
         | 
| 25 | 
            +
                        question_response_pairs = node.xpath('.//xmlns:correctResponse//xmlns:value').map do |value|
         | 
| 26 | 
            +
                          value.content.split
         | 
| 27 | 
            +
                        end
         | 
| 28 | 
            +
                        question_response_id_mapping = Hash[question_response_pairs]
         | 
| 29 | 
            +
                        data = question_response_id_mapping.reduce({}) do |acc, pair|
         | 
| 30 | 
            +
                          question_id, answer_id = pair
         | 
| 31 | 
            +
                          acc.update question_id => choices_by_identifier[answer_id].content
         | 
| 32 | 
            +
                        end
         | 
| 33 | 
            +
                        [ScoringData.new(data, 'directedPair')]
         | 
| 34 | 
            +
                      end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                      private
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                      def choices_by_identifier
         | 
| 39 | 
            +
                        choices = node.xpath('.//xmlns:simpleAssociableChoice')
         | 
| 40 | 
            +
                        @choices_by_identifier ||= choices.reduce({}) do |acc, choice|
         | 
| 41 | 
            +
                          acc.update choice.attributes['identifier'].value => choice
         | 
| 42 | 
            +
                        end
         | 
| 43 | 
            +
                      end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                      def answer_nodes
         | 
| 46 | 
            +
                        node.xpath('.//xmlns:matchInteraction//xmlns:simpleMatchSet[2]//xmlns:simpleAssociableChoice')
         | 
| 47 | 
            +
                      end
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
            end
         | 
    
        data/lib/qti.rb
    CHANGED
    
    | @@ -52,14 +52,20 @@ require 'qti/models/manifest' | |
| 52 52 | 
             
            require 'qti/models/base'
         | 
| 53 53 |  | 
| 54 54 | 
             
            require 'qti/v1/models/base'
         | 
| 55 | 
            +
            require 'qti/v1/models/interactions/base_interaction'
         | 
| 56 | 
            +
            require 'qti/v1/models/interactions/choice_interaction'
         | 
| 55 57 | 
             
            require 'qti/v1/models/assessment'
         | 
| 56 58 | 
             
            require 'qti/v1/models/assessment_item'
         | 
| 57 59 | 
             
            require 'qti/v1/models/choices/logical_identifier_choice'
         | 
| 60 | 
            +
            require 'qti/v1/models/scoring_data'
         | 
| 58 61 |  | 
| 59 62 | 
             
            require 'qti/v2/models/base'
         | 
| 63 | 
            +
            require 'qti/v2/models/interactions/base_interaction'
         | 
| 60 64 | 
             
            require 'qti/v2/models/choices/simple_choice'
         | 
| 65 | 
            +
            require 'qti/v2/models/choices/simple_associable_choice'
         | 
| 61 66 | 
             
            require 'qti/v2/models/assessment_item'
         | 
| 62 67 | 
             
            require 'qti/v2/models/assessment_test'
         | 
| 68 | 
            +
            require 'qti/v2/models/scoring_data'
         | 
| 63 69 |  | 
| 64 70 | 
             
            require 'zip'
         | 
| 65 71 | 
             
            require 'qti/exporter'
         | 
| @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            <?xml version="1.0" encoding="UTF-8"?>
         | 
| 2 | 
            +
            <questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
         | 
| 3 | 
            +
              <item title="" ident="4772">
         | 
| 4 | 
            +
                <itemmetadata>
         | 
| 5 | 
            +
                  <qtimetadata>
         | 
| 6 | 
            +
                    <qtimetadatafield>
         | 
| 7 | 
            +
                      <fieldlabel>question_type</fieldlabel>
         | 
| 8 | 
            +
                      <fieldentry>matching_question</fieldentry>
         | 
| 9 | 
            +
                    </qtimetadatafield>
         | 
| 10 | 
            +
                  </qtimetadata>
         | 
| 11 | 
            +
                </itemmetadata>
         | 
| 12 | 
            +
                <presentation>
         | 
| 13 | 
            +
                  <material>
         | 
| 14 | 
            +
                    <mattext texttype="text/html">Match the statements with the correct type of microscope</mattext>
         | 
| 15 | 
            +
                  </material>
         | 
| 16 | 
            +
                  <response_lid ident="question_1" rcardinality="Single">
         | 
| 17 | 
            +
                    <material>
         | 
| 18 | 
            +
                      <mattext texttype="text/plain">Light Microscope</mattext>
         | 
| 19 | 
            +
                    </material>
         | 
| 20 | 
            +
                    <render_choice>
         | 
| 21 | 
            +
                      <response_label ident="8440">
         | 
| 22 | 
            +
                        <material>
         | 
| 23 | 
            +
                          <mattext texttype="text/plain">Magnify up to about 400 times. Sometimes requires colored staining of cells.</mattext>
         | 
| 24 | 
            +
                        </material>
         | 
| 25 | 
            +
                      </response_label>
         | 
| 26 | 
            +
                      <response_label ident="5653">
         | 
| 27 | 
            +
                        <material>
         | 
| 28 | 
            +
                          <mattext texttype="text/plain">Uses a beam of electrons. Can provide details of cells' internal structure.</mattext>
         | 
| 29 | 
            +
                        </material>
         | 
| 30 | 
            +
                      </response_label>
         | 
| 31 | 
            +
                    </render_choice>
         | 
| 32 | 
            +
                  </response_lid>
         | 
| 33 | 
            +
                  <response_lid ident="question_2" rcardinality="Single">
         | 
| 34 | 
            +
                    <material>
         | 
| 35 | 
            +
                      <mattext texttype="text/plain">Electron Microscopes</mattext>
         | 
| 36 | 
            +
                    </material>
         | 
| 37 | 
            +
                    <render_choice>
         | 
| 38 | 
            +
                      <response_label ident="8440">
         | 
| 39 | 
            +
                        <material>
         | 
| 40 | 
            +
                          <mattext texttype="text/plain">Magnify up to about 400 times. Sometimes requires colored staining of cells.</mattext>
         | 
| 41 | 
            +
                        </material>
         | 
| 42 | 
            +
                      </response_label>
         | 
| 43 | 
            +
                      <response_label ident="5653">
         | 
| 44 | 
            +
                        <material>
         | 
| 45 | 
            +
                          <mattext texttype="text/plain">Uses a beam of electrons. Can provide details of cells' internal structure.</mattext>
         | 
| 46 | 
            +
                        </material>
         | 
| 47 | 
            +
                      </response_label>
         | 
| 48 | 
            +
                      <response_label ident="7777">
         | 
| 49 | 
            +
                        <material>
         | 
| 50 | 
            +
                          <mattext texttype="text/plain">A distractor answer.</mattext>
         | 
| 51 | 
            +
                        </material>
         | 
| 52 | 
            +
                      </response_label>
         | 
| 53 | 
            +
                    </render_choice>
         | 
| 54 | 
            +
                  </response_lid>
         | 
| 55 | 
            +
                </presentation>
         | 
| 56 | 
            +
                <resprocessing>
         | 
| 57 | 
            +
                  <outcomes>
         | 
| 58 | 
            +
                    <decvar maxvalue="100" minvalue="0" varname="SCORE" vartype="Decimal"/>
         | 
| 59 | 
            +
                  </outcomes>
         | 
| 60 | 
            +
                  <respcondition>
         | 
| 61 | 
            +
                    <conditionvar>
         | 
| 62 | 
            +
                      <varequal respident="question_1">8440</varequal>
         | 
| 63 | 
            +
                    </conditionvar>
         | 
| 64 | 
            +
                    <setvar varname="SCORE" action="Add">50.00</setvar>
         | 
| 65 | 
            +
                  </respcondition>
         | 
| 66 | 
            +
                  <respcondition>
         | 
| 67 | 
            +
                    <conditionvar>
         | 
| 68 | 
            +
                      <varequal respident="question_2">5653</varequal>
         | 
| 69 | 
            +
                    </conditionvar>
         | 
| 70 | 
            +
                    <setvar varname="SCORE" action="Add">50.00</setvar>
         | 
| 71 | 
            +
                  </respcondition>
         | 
| 72 | 
            +
                </resprocessing>
         | 
| 73 | 
            +
              </item>
         | 
| 74 | 
            +
            </questestinterop>
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            <?xml version="1.0" encoding="ISO-8859-1"?>
         | 
| 2 2 |  | 
| 3 | 
            -
            <questestinterop>
         | 
| 3 | 
            +
            <questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
         | 
| 4 4 |  | 
| 5 5 | 
             
            <assessment title="Quiz" ident="A1001">
         | 
| 6 6 | 
             
            <section title="Main" ident="S1002">
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            <?xml version="1.0" encoding="ISO-8859-1"?>
         | 
| 2 2 |  | 
| 3 | 
            -
            <questestinterop>
         | 
| 3 | 
            +
            <questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
         | 
| 4 4 |  | 
| 5 5 | 
             
            <assessment title="1.2 Import Quiz" ident="A1001">
         | 
| 6 6 | 
             
            <section title="Main" ident="S1002">
         |