Skip to content

Instantly share code, notes, and snippets.

@ehowe
Created August 23, 2016 16:17
Show Gist options
  • Save ehowe/98ad9afe8f961cc1c638f2cad5f748aa to your computer and use it in GitHub Desktop.
Save ehowe/98ad9afe8f961cc1c638f2cad5f748aa to your computer and use it in GitHub Desktop.
require 'nokogiri'
require 'awesome_print'
require 'pry'
require 'securerandom'
require 'fileutils'
class Item
attr_accessor :type, :answer, :answer_options, :complexity, :subject, :ahead, :feedback, :contents, :question, :test_name, :number, :matching_pairs, :id
def self.matching_items_to_singular_item(items)
matching_pairs = items.map do |i,hash|
question_id = SecureRandom.uuid
answer_id = SecureRandom.uuid
{question_id => i.question, answer_id => i.answer}
end
new("matching", nil, items.first.test_name, matching_pairs: matching_pairs)
end
def initialize(type, contents, test_name, options={})
@type = type
@contents = contents
@test_name = test_name
@matching_pairs = options[:matching_pairs]
@id = "i#{SecureRandom.hex(9)}"
unless @matching_pairs
generic_attributes
send("#{type}_attributes")
end
end
def generic_attributes
@number, @question = contents[0].split(/\.\s/).map(&:strip)
@answer = sanitized_attribute('Ans') || sanitized_attribute("Answer")
@ahead = sanitized_attribute('Ahead')
@complexity = sanitized_attribute('Complexity')
@subject = sanitized_attribute('Subject')
@feedback = sanitized_attribute('Feedback')
end
def multiple_choice_attributes
@answer_options = contents[1..4].map { |a| a.split(/([A-Z]\)|\[[0-9]+\])\s/).last.strip }
end
def short_answer_attributes
end
def matching_attributes
end
def sanitized_attribute(name)
attribute = contents.detect { |c| c.match(/^#{name}:/) }
attribute && attribute.split(":\s")[1] && attribute.split(":\s")[1].strip
end
def answer_choice
case answer
when 'A' then 1
when 'B' then 2
when 'C' then 3
when 'D' then 4
end
end
def to_xml
send("#{type}_qti")
end
def multiple_choice_qti
Nokogiri::XML::Builder.new do |xml|
xml.assessmentItem("xmlns" => "http://www.imsglobal.org/xsd/imsqti_v2p1", "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xmlns:m" => "http://www.w3.org/1998/Math/MathML", "xsi:schemaLocation" => "http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/qti/qtiv2p1/imsqti_v2p1.xsd", "identifier" => id, "title" => "#{test_name} #{number}", "adaptive" => "false", "timeDependent" => "false") do
xml.responseDeclaration("baseType" => "identifier", "cardinality" => "single", "identifier" => "RESPONSE") do
xml.correctResponse do
xml.value do
xml.cdata "choice_#{answer_choice}"
end
end
end
xml.outcomeDeclaration("cardinality" => "single", "baseType" => "float", "identifier" => "SCORE")
xml.itemBody do
xml.div("class" => "grid-row") do
xml.div("class" => "col-12") do
xml.choiceInteraction("shuffle" => false, "maxChoices" => "1", "orientation" => "vertical", "responseIdentifier" => "RESPONSE", "minChoices" => "1", "class" => "list-style-lower-alpha") do
xml.prompt do
xml.p question
end
answer_options.each_with_index do |item,i|
xml.simpleChoice("fixed" => "false", "showHide" => "show", "identifier" => "choice_#{i + 1}") do
xml.p item
end
end
end
end
end
end
xml.responseProcessing("template" => "http://www.imsglobal.org/question/qti_v2p1/rptemplates/match_correct")
end
end.to_xml
end
def short_answer_qti
Nokogiri::XML::Builder.new do |xml|
xml.assessmentItem("xmlns" => "http://www.imsglobal.org/xsd/imsqti_v2p1", "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xmlns:m" => "http://www.w3.org/1998/Math/MathML", "xsi:schemaLocation" => "http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/qti/qtiv2p1/imsqti_v2p1.xsd", "identifier" => id, "title" => "#{test_name} #{number}", "adaptive" => "false", "timeDependent" => "false") do
xml.outcomeDeclaration("cardinality" => "single", "baseType" => "float", "identifier" => "SCORE")
xml.itemBody do
xml.div("class" => "grid-row") do
xml.div("class" => "col-12") do
xml.p question
end
end
end
end
end.to_xml
end
def matching_qti
raise "not the correct item type" unless @matching_pairs
Nokogiri::XML::Builder.new do |xml|
xml.assessmentItem("xmlns" => "http://www.imsglobal.org/xsd/imsqti_v2p1", "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xmlns:m" => "http://www.w3.org/1998/Math/MathML", "xsi:schemaLocation" => "http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/qti/qtiv2p1/imsqti_v2p1.xsd", "identifier" => "choice_#{id}", "title" => "#{test_name} #{number}", "adaptive" => "false", "timeDependent" => "false") do
xml.responseDeclaration("baseType" => "pair", "cardinality" => "multiple", "identifier" => "RESPONSE") do
xml.correctResponse do
@matching_pairs.each do |hash|
xml.value do
xml.cdata hash.keys.join(" ")
end
end
end
end
xml.outcomeDeclaration("cardinality" => "single", "baseType" => "float", "identifier" => "SCORE")
xml.itemBody do
xml.div("class" => "grid-row") do
xml.div("class" => "col-12") do
xml.associateInteraction("shuffle" => "false", "maxAssociations" => "0", "minAssociations" => "0", "responseIdentifier" => "RESPONSE") do
xml.prompt do
xml.p "Match the following terms"
end
@matching_pairs.each do |hash|
hash.each_pair do |id, value|
xml.simpleAssociableChoice("fixed" => "false", "showHide" => "show", "matchMax" => "1", "matchMin" => "1", "identifier" => "choice_#{id}") do
xml.text value
end
end
end
end
end
end
end
xml.responseProcessing("template" => "http://www.imsglobal.org/question/qti_v2p1/rptemplates/match_correct")
end
end.to_xml
end
end
class Parser
attr_reader :file, :items, :index, :where_am_i, :folder
def initialize(file)
@file = file
@items = []
@index = 10
@where_am_i = "multiple_choice"
@folder = contents[9].split(": ")[1]
end
def contents
@contents ||= File.open(file).readlines.map(&:strip).reject(&:empty?)
end
def parse
until self.where_am_i.nil?
send("parse_#{self.where_am_i}")
end
end
def delimiters
["Multiple Choice", "Short Answer", "Matching"]
end
def how_far_to_go
how_far_to_go = 1
until contents[@index + how_far_to_go].nil? || delimiters.include?(contents[@index + how_far_to_go]) || contents[@index + how_far_to_go].match(/^\d+\./)
how_far_to_go += 1
end
how_far_to_go
end
def parse_multiple_choice
until contents[@index].nil? || contents[@index].match(/^\d+\./).nil?
how_far = how_far_to_go
@items << Item.new("multiple_choice", contents[@index..(@index + (how_far - 1))], test_name)
@index += how_far
end
@where_am_i = contents[@index] && contents[@index].downcase.gsub(" ", "_")
@index += 1
end
def parse_short_answer
until contents[@index].nil? || contents[@index].match(/^\d+\./).nil?
how_far = how_far_to_go
@items << Item.new("short_answer", contents[@index..(@index + (how_far - 1))], test_name)
@index += how_far
end
@where_am_i = contents[@index] && contents[@index].downcase.gsub(" ", "_")
@index += 1
end
def parse_matching
matching_items = []
until contents[@index].nil? || contents[@index].match(/^\d+\./).nil?
how_far = how_far_to_go
matching_items << Item.new("matching", contents[@index..(@index + (how_far - 1))], test_name)
@index += how_far
end
@items << Item.matching_items_to_singular_item(matching_items)
@index += 1
@where_am_i = contents[@index] && contents[@index].downcase.gsub(" ", "_")
end
def test_name
if file.split(".").count == 2
file.split(".").first
else
"#{file.split(".")[0]} #{file.split(".")[1]}"
end
end
def self.manifest(items)
Nokogiri::XML::Builder.new do |xml|
xml.manifest("xmlns" => "http://www.imsglobal.org/xsd/imscp_v1p1", "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation" => "http://www.imsglobal.org/xsd/imscp_v1p1 http://www.imsglobal.org/xsd/qti/qtiv2p1/qtiv2p1_imscpv1p2_v1p0.xsd", "identifier" => "MANIFEST-tao#{SecureRandom.hex(7)}-43353328") do
xml.metadata do
xml.schema "QTIv2.1 Package"
xml.schemaversion "1.0.0"
end
xml.organizations
xml.resources do
items.each do |item|
xml.resource("identifier" => item.id, "type" => "imsqti_item_xmlv2p1", "href" => "#{item.id}/qti.xml") do
xml.file("href" => "#{item.id}/qti.xml")
end
end
end
end
end.to_xml
end
def self.dump(items)
Dir.mktmpdir("qti") do |dir|
File.open("#{dir}/imsmanifest.xml", "w") { |f| f.puts manifest(items) }
items.each do |item|
path = "#{dir}/#{item.id}"
item_dir = Dir.mkdir(path)
File.open("#{path}/qti.xml", "w") { |f| f.puts item.to_xml }
end
`rm /Users/eugene/class.zip`
`cd #{dir} && zip -r /Users/eugene/class.zip .`
end
end
end
files = Dir.glob("*.txt")
items = []
files.each do |file|
parser = Parser.new(file)
parser.parse
items += parser.items
end
Parser.dump(items)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment