code

The code for the creator/converter is below, and comes in two parts: cqfacility.py, which is largely just a menu in a main() method, and cqFunctions.py, where all the work is done.

The conversion is done on the .txt file that is created in WebCT Designer Options Question database. Select your type 'C' questions, and select "Download questions" on the right under "Options: Question".

You will see that there are three more files necessary: questionTop.xml, questionBottom.xml, and questionMiddle.xml. I can email them to you if you want. However, you can create these yourselves as follows:

  1. In Sakai, create a one-question FIB question and export it to a file. Keep it simple ("What is a color" "*blue"), as you are going to throw that part away anyway.
  2. questionTop is created by taking everything up to "What is a color" and saving it as questionTop.xml.
  3. middle is created by taking everything after "What is a color" up to "blue". By the way, questions and answers are found in the code with CDATA[MGT:question] or CDATA[MGT:answer]. You'll have to look for it, there is alot of code in there!
  4. bottom is created by, you guessed it, taking everything after the answer to the end of the file and saving it as questionBottom.xml.
    The reason I don't post it here is because a one question quiz generates around 1000 lines in Sakai!

Please check it over, use it, tell me what you think. It works fine for what it is, but the lack of numeric question types in question pools leaves for rather strict grading standards for math questions that might otherwise allow for some tolerance in the answer.

Also, I realize it is perhaps sloppy in places, and there were improvements in mind, but there was a time-crunch before the conference so I had to cut off improvements at some point, and just aim for being functional!

Write me at roryneil@uwyo.edu if you have any questions or suggestions.

cqfacility.py

# cqfacility.py
# Rory Jarrard
# 2 NOV 2008

from cqFunctions import *
import sys
import os

def main():
	choice = ''

	#prompt menu	
	while True:
		os.system('clear')
		choice = intest(menu)
		if choice == '1':
			convertWCT()
		elif choice == '2':
			createCQ()
		elif choice == '3':
			print "\nExiting at user request.\n"
			sys.exit()
		else:
			choice = intest("\nThat is not a valid choice.\nEnter 1, 2, or 3: ")

if __name__ == '__main__':
	main()

cqFunctions.py

# menu definition

from math import *
from string import replace
from random import uniform, randint
import os

menu =  '*****************************************************\n' + \
		'*                                                   *\n' + \
		'*  Welcome to the Calculated Questions Facility.    *\n' + \
		'*                                                   *\n' + \
		'*  Please decide from:                              *\n' + \
		'*                                                   *\n' + \
		'*     1]  Convert a WebCT "tagged text" file.       *\n' + \
		'*     2]  Create a new Question Pool.               *\n' + \
		'*     3]  Exit the facility.                        *\n' + \
		'*         (CTRL+c or CTRL+d will exit at any time)  *\n' + \
		'*                                                   *\n' + \
		'*****************************************************\n\n> '

inst = '********************************************************************\n' + \
	   '*                                                                  *\n' + \
	   '*  Enter your question, following these rules:                     *\n' + \
       '*                                                                  *\n' + \
	   '*     1]  Enclose variables in brackets.                           *\n' + \
	   '*         ex: What is the volume of a cube with side {s}?          *\n' + \
	   '*                                                                  *\n' + \
	   '*         *** Note to WebCT users: this is the same syntax         *\n' + \
	   '*             you are used to.                                     *\n' + \
	   '*                                                                  *\n' + \
	   '*     2]  When simply referring to variable, do not use brackets.  *\n' + \
	   '*         ex: \"Express s as an integer\", not \"Express {s}...\"      *\n' + \
	   '*                                                                  *\n' + \
	   '*     3]  Enter CTRL+c or CTRL+d to exit at any time.              *\n' + \
	   '*                                                                  *\n' + \
	   '********************************************************************\n\n> '
    
def intest(string):
    """Tests for interrupts on input"""

    try:
        x = raw_input(string)
        while x == '':
            x = raw_input('No input was detected. Please re-enter: ')
    except (EOFError, KeyboardInterrupt):
        print '\nClosing program at user request.\n'
        exit(0)
    return x

def createCQ():
	again = 1
	question = ''			# user-entered question
	variables = []			# string variable names from question
	variable_limits = {}	# variable min/max and precision
	pool = []				# random var values and answers
	formula = ''			# user entered formula
	author = ''				# sakai username

	while (again == 1):
		os.system('clear')

		# Get question from instructor
		question = intest(inst)

		while not count_pairs(question):
			question = intest('\nThat had an uneven pairing of brackets. Please re-enter:\n> ')

		# Parse out variables
		variables = extract_variables(question)
    
    	# Prompt for variable min/max/dec
		variable_limits = set_limits(variables)

		# Prompt for formula
		formula = intest('\nEnter formula, again using brackets\n> ')

    	# Create pool
		pool = create_pool(question, formula, variables, variable_limits)

		# Create a .txt or .xml file suitable for pasting or importing to Sakai
		filename = intest('\nWhat name do you want for this pool? (Do not include file extension)\n> ')
		type = int(intest('Do you want\n\t1] .txt file\n\t2] IMS QTI-compliant .xml file?\n> '))

		if type == 2:
			author = intest('What is your WyoSakai username?\n> ')
			create_xml(filename, author, pool)
		elif type == 1:
			create_txt(filename, pool)

		print "*** " + filename + " was created! ***\n\n"

		again = intest('Do you want to:\n\t1)  Create another pool.\n\t2)  Exit to main\n> ')
		while again not in ['1', '2']:
			again = intest('Not a valid choice.\nPlease enter 1 or 2: ')
		again = int(again)

def count_pairs(string):
    """ Checks to make sure of equal bracket pairings. Returns true if equal."""
    
    count = 0
    for letter in string:
        if letter == '{':
            count += 1
        elif letter == '}':
            count -= 1
    if count == 0:
        return True
    else:
        return False

def extract_variables(string):
    """Extracts variables from string as content between brackets. Returns list."""
    lst = []
    tmp = ''
    tmpindex = 0
    for index in range(len(string)):
        if string[index] == '{':
            tmpindex = index + 1
            while string[tmpindex] != '}':
                tmp += string[tmpindex]
                tmpindex += 1
        if tmp: lst.append(tmp)
        tmp = ''
        index = tmpindex + 1
    print "\nThe following variables have been detected:"
    for item in lst:
        print '\t', item
    print
    return lst

def set_limits(lst):
    """Sets min/max for lst items.Sets decimal precision.Returns dictionary"""
    temp_dct = {}
    print "\nSetting limits for variables.\n----------------------------" + \
          "\nIf the number you entered contains a decimal, it will\n" + \
          "be considered a float.  Otherwise, it will be treated as\n" + \
          "an integer.  Be sure minimum and maximum number types match.\n"

    for var in lst:
        temp_dct[var] = {'min':'', 'max':''}
    for item in temp_dct:
        temp_dct[item]['min'] = eval(intest('\nWhat is the minimum value for ' + item + '?\n> '))
        if str(temp_dct[item]['min'])[-1] == '.':
            temp_dct[item]['min'] = float(str(temp_dct[item]['min']) + '0')
        temp_dct[item]['max'] = eval(intest('What is the maximum value for ' + item + '?\n> '))
        if str(temp_dct[item]['max'])[-1] == '.':
            temp_dct[item]['max'] = float(str(temp_dct[item]['max']) + '0')
	#check for type matching
        while type(temp_dct[item]['min']) != type(temp_dct[item]['max']):
            print("\nThose values were of different numerical types.  Please reenter.")
            temp_dct[item]['min'] = eval(intest('\nWhat is the minimum value for ' + item + '?\n> '))
            temp_dct[item]['max'] = eval(intest('What is the maximum value for ' + item + '?\n> '))
    temp_dct['dec'] = eval(intest('\nHow many decimal places in answer?\n> '))
    # set min and max with this decimal precision
    for item in temp_dct:
        if item == 'dec':
            pass
        elif type(temp_dct[item]['min']) == int:
            pass
        else:
            evaluator = "%." + str(temp_dct['dec']) + "f"
            temp_dct[item]['min'] = float(evaluator % temp_dct[item]['min'])
            temp_dct[item]['max'] = float(evaluator % temp_dct[item]['max'])
    
    return temp_dct

def create_pool(question, formula, variables, varlim):
    """Creates a number of questions with random variable values. Returns list"""
    pool = []
    ques = question
    data = {}
    ans = formula
    evaluator = "%." + str(varlim['dec']) + "f"
    num = input('\nHow many versions of this question do you want created?\n> ')

    for n in range(num):
        for var in variables:
            #print type(varlim[var]['min'])
            if type(varlim[var]['min']) == int:
                val = str(randint(varlim[var]['min'], varlim[var]['max']))
                # print "value =", val
            else:
                val = uniform(varlim[var]['min'], varlim[var]['max'])
                val = evaluator % val
                # print "value =", val
            ques = replace(ques, '{'+var+'}', val)
            ans = replace(ans, '{'+var+'}', val)

        ans = str(evaluator % eval(ans))
        data['ques'] = ques
        data['ans'] = ans
        pool.append(data)

        ques = question
        data = {}
        ans = formula

    return pool

def create_xml(filename, author, pool):
    """This function will append .xml to filename, and create the full
       assessment code for these questions, suitable for import from
       Sakai"""
    f = open(filename + '.xml', 'w')
    t = open('top.xml', 'r')
    for line in t.readlines():
        if 'xyzzy' in line:
            line = replace(line, 'xyzzy', filename)
        if 'yzzyx' in line:
            line = replace(line, 'yzzyx', author)
        f.write(line)
    
    for item in pool:
        qt = open('questionTop.xml','r')
        for line in qt.readlines():
            f.write(line)
        qt.close()
        print >> f, item['ques']
        qm = open('questionMiddle.xml','r')
        for line in qm.readlines():
            f.write(line)
        qm.close()
        print >> f, item['ans']
        qb = open('questionBottom.xml','r')
        for line in qb.readlines():
            f.write(line)
        qb.close()
    b = open('bottom.xml','r')
    for line in b.readlines():
        f.write(line)
    b.close()

    f.close()

def create_txt(filename, pool):
    """Will create txt file suitable for copy/paste into Sakai"""
    filename = filename + '.txt'
    f = open(filename, 'w')

    index = 1
    for item in pool:
        print >> f, str(index) + '.'
        print >> f, item['ques']
        print >> f, '*' + item['ans'] + '\n'
        index += 1

class Question(object):
    """Puts questions into human-readable form suitable for saving to
    file for copy/paste to Sakai."""
    def __init__(self, ques, index):
        self.title = 'Question' + str(index)
        self.question = ques['Question']
        self.formula = ques['Formula']
        self.dec = ques['Decimals']
        self.values = ques['Values']
        self.varNames = ques['Variables']
        self.varVals = {}
        for var in self.varNames:
            self.varVals[var] = ques[var]

    def create(self, num):
        question = str(num + 1) + '.' + ' ' + self.question
        for var in self.varNames:
            strng = '{' + var + '}'
            question = question.replace(strng, str(self.varVals[var][num]))
        return question

    def answer(self, num):
        answer = self.formula
        # raw_input('answer is ' + answer)
        for var in self.varNames:
            strng = '{' + var + '}'
            answer = answer.replace(strng, str(self.varVals[var][num]))
        return round(eval(answer), self.dec)

def convertWCT():
    filename = intest('What is the name of your exported file\n> ')
    allQuestions = [] # used to hold individual questions

    # gather all data from file
    allData = open(filename, 'r').readlines()
##    for line in allData:
##        print line

    # parse file into individual questions
    parseQuestions(allQuestions, allData)

    # create questions
    x = 1
    print
    for question in allQuestions:
        q = Question(question, x)

        outfile = q.title + '.txt'
        f = open(outfile, 'w')

        print "Creating " + outfile + "...",
        
        for num in range(question['Values']):
            print >> f, q.create(num)
            print >> f, '*', q.answer(num)

        print "Done!\n\n"


        x += 1
        f.close()

    print "\nSave the above files to a secure location (read: not in this folder!) as\n" + \
          "they will be overwritten the next time the facility is run.\n\n" + \
          "--------------------------------------------------------------------------"

    again = intest('\nDo you want to:\n\t1)  Convert another file.\n\t2)  Exit to main\n> ')
    while again not in ['1', '2']:
        again = intest('Not a valid choice.\nPlease enter 1 or 2: ')
        again = int(again)

def parseQuestions(ques, data):
    """Saves all data from one questions, sends it to be parsed of all
    extraneous information, and appends that to 'ques'.  Performs this on
    all questions from 'data'."""
    seg = []
    for line in data:
        if line == '\n':
            pass
        elif '# End' not in line:
            # add to current quesion
            seg.append(line)
        else:
            # one question completed, send for parsing, add to 'ques'
            seg = parseSeg(seg)
            ques.append(seg)
            # print seg
            # raw_input('')
            seg = []

def parseSeg(seg):
    """Removes all but questions, formula and variables from 'seg'."""
    this = {} # used to hold all relevant information
    vars = [] # holds all variable names parsed from 'seg'
    start = None

    for index in range(len(seg)):
        # one line from 'seg'
        if 'QUESTION' in seg[index]:
            this['Question'] = seg[index + 1].strip('\n')
        if 'FORMULA' in seg[index]:
            this['Formula'] = seg[index][9:].strip('\n')
        if 'MIN' in seg[index]:
            vars.append(getVar(seg[index]))
        if 'VALUES' in seg[index]:
            start = index + 1
            values = int(seg[index][8:].strip('\n'))
            this['Values'] = values
        if 'ANS-' in seg[index]:
            end = index
            this['Decimals'] = int(seg[index][9:].strip('\n'))

    for var in vars:
        this[var] = getValues(var, seg[start:end])

    this['NumVars'] = len(vars)
    this['Variables'] = vars

##    for key in this:
##        print key
##        print this[key]

    return this

def getVar(line):
    """Returns name of one variable extracted from 'line'."""
    var = ''
    for x in range(len(line)):
        if line[x] == '-':
            var = line[1:x]
    return var

def getValues(var, seg):
    """Returns all possible values for a given variable as exported
    from WebCT."""
    vals = []
    for line in seg:
        if var in line:
            for x in range(len(line[1:])):
                if line[x] == ':':
                    index = x + 1
            vals.append(float(line[index:].strip('\n')))
    
    return vals