Created
August 23, 2016 16:17
-
-
Save ehowe/98ad9afe8f961cc1c638f2cad5f748aa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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