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:
- 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.
- questionTop is created by taking everything up to "What is a color" and saving it as questionTop.xml.
- 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!
- 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