:Wright State University Lake Campus/2019-5/QuizSoftware codes


9 May 2019 (UTC) edit

makeExam.py edit

##This code must be run in a folder that contains:
##    1. ./bank (contains .tex files for all bank wikiquizzes)
##    2. ./bank/images (images contain all the image files.
##    3. ./input/xxx.csv (where one or more csv files are situated)
##    4. ./output (where the four xxx.tex files will be created)
##    5. ./output/images (I currently requre to copies of the images)
##The extra images file (in output) is not required for this program to run,
## but is required to run the output xxx.tex files that are created. Also
## created in the output file is an empty ./output/xxx directory where
## the pdf files created by LaTex can be placed (I use Miktex)
##
##Also in the directory that holds this program (program.py) is another
## program called template.py.  Use template.py to create the xxx.csv files 
## used to create the xxx.csv files used by program.py.

import os, sys, csv, re, time, copy, shutil, random#(re not used yet)
global debugmode
debugmode=False
Nversions=6
Nversions=int(input("Nversions: "))

statement='%statement\n'
statement+='Though posted on Wikiversity, this document was created without '
statement+='wikitex using Python to write LaTeX markup.  With a bit more development '
statement+='it will be possible for users to download and use software that will '
statement+='permit them to create, modify, and print their own versions of this document.\n'
statement+='% insert more here...\\\\\n'
##This code opens a testfile (e.g. anyTest.csv) and
##verifies that it is properly formatted.  It checks that:
##1. The testName is in the 01 cell of the csv file
##2. The number of questions in the bank in the 00 file is correct
##3. Each available question has a top row integral question number  
## If possible it fixes the error and creates anyTestUPGRADE.csv
####Code requires ./bank/ and ./input/template/ directories
## getInfo returns testName
## getCheckData returns csvData (what might be on test)
class Question:
    def __init__(self):
        self.quizName =""
        self.number=0 #
        self.quizType = ""
        self.QA = [] #question and answer
        self.Q=[] #question only
        self.A=[] #answer only
        self.Nrenditions = 1
class Quiz:
    def __init__(self):
        self.quizName =""
        self.quizType = ""
        self.questions = []#Question() clas list
        self.Nquestions=0#number of questions
class CsvInfo:
    def __init__(self):
        self.comments=[]#col 1a       
        self.quizNames=[]#col 2b
        self.selections=[]#col 3c (Sel)
        self.Nquestions=[]#col 4d (available)
        self.types=[]# t,n,c col 4e 
        self.choices=[] #list starts at 5f
def whatis(string, x):
    print(string+' value=',repr(x),type(x))
    return string+' value='+repr(x)+repr(type(x))
def getFromChoices(first,second,num2pick): #select num2pick items
    random.shuffle(first)#list of integers first priority
    random.shuffle(second)#list of integers second priority
    allofem=(first+second)

    Nmax=min(len(allofem),num2pick)#Don't pick more than available
    selected=allofem[0:Nmax]
    random.shuffle(selected)
    return selected
def getTestName():
    if debugmode: #set testName to sample and return
        return "s_ample"
    availableTests=[]
    for item in os.listdir('./input'):
        if item.endswith(".csv"):
            availableTests.append(item[:-4])
    print("availableTests: ", availableTests)  
    testName=input('enter testName ')   
    if testName not in availableTests:
        testName="s_ample"
        print("testName taken to be",testName)
        if testName not in availableTests:
            print(testName, " not available in ./input")
            sys.exit("testName not available in ./input")
    return testName
def getCsv(csvInfo,testName):
    string=os.path.join("./input",testName+'.csv')
    with open(string,newline='') as fin:
        reader = csv.reader(fin)
        csvData=list(reader)
    #rows & columns indexed as follows
    cvsRows=len(csvData)#number of rows: index as nrow
    cvsCols=len(csvData[0])#num cols: index as ncol
    #print('csvData dimensions: Nrow,Ncol',cvsRows,cvsCols)   
    commentColA=[]#0a comments used 
    quizNameColB=[]#1b quiznames list
    selectionColC=[]#2c num Sel=S=selected for test
    NavailableColD=[]#3d num questions in quiz (bank)
    typeColE=[]#4e quizType list
    choiceListF=[]#5f lists len(NQues) w/ 0,1,or 2
    ##The first row is different:
    ##0A=number of questions on test
    ##0B=testName
    ##0C="Sel" (either 0,1,2 where "blank" means 0
    ##0D=number of questions in the bank
    ##0E="t" (type)
    ##0F,0G,...=1,2,...          
    for nrow in range(0,cvsRows):
        commentColA.append(csvData[nrow][0])#0a
        quizNameColB.append(csvData[nrow][1])   #1b
        selectionColC.append(csvData[nrow][2])   #2c        
        NavailableColD.append(csvData[nrow][3])  #3d
        typeColE.append(csvData[nrow][4])   #4e
        choiceListF.append(csvData[nrow][5:cvsCols])
    csvInfo.comments=commentColA
    csvInfo.quizNames=quizNameColB
    csvInfo.selections=selectionColC
    csvInfo.Nquestions=NavailableColD
    csvInfo.types=typeColE
    csvInfo.choices=choiceListF
    return [csvInfo,cvsRows,csvData]              #ends getCvs
def prefixUnderscore(string):
    string=string.replace('_','\\_')
    return string    
def findLatexWhole(pre,string,post):
    #returns list of indices between the pre and post
    #tags.  Whole includes the tags
    mylist=[]
    tag=pre+r'(.*?)'+post
    matches=re.finditer(tag,string,re.S)
    for item in matches:
        mylist.append([item.start(),item.end()])
    #mylist is a lists of two element lists (start/st
    #the two element list is the start/stop point in string
    #mylist is as long as there are instances of the tag
    return mylist  #ends findLatexWhole
def findLatexBetween(pre,strIn,post):
    #returns list of indeces between the pre and post
    #tags.  Between excludes the tags
    mylist=[]
    tag=pre+r'(.*?)'+post
    matches=re.finditer(tag,strIn,re.S)
    for item in matches:
    #To get "between" we subtract out the pre and post
    #part of the tag.  We need to count the backslashes
    #in order to compensate for the extra \ in the \\ escape
        n1=item.start()+len(pre)-pre.count(r'\\')
        n2=item.end()-len(post)+post.count(r'\\')#as before:
    #mylist is a lists of two element lists (start/st
    #the two element list is the start/stop point in string
    #mylist is as long as there are instances of the tag
        mylist.append([n1,n2])
    return mylist                        #ends findLatexBetween
def QA2QandA(string): 
    #Define 4 patterns:
    beginStr=r'\\begin{choices}'
    endStr=r'\\end{choices}'
    CoStr=r'\\CorrectChoice '
    chStr=r'\\choice '
    #get Q=question
    n=string.find('\\begin{choices}')
    Q=string[0:n]
    #get A=string of answers
    #newStr defines the new search parameter as 1st answer
    pat=CoStr+'|'+chStr+'|'+endStr+'(.*?)'#fails-not greedy   
    iterations=re.finditer(pat,string,re.S)
    nList=[0]
    A=[]  
    for thing in iterations:
        nList.append(thing.start())
    #for i in range(len(nList)): #crashes as expected
    for i in range(1,len(nList)-1):
        A.append(string[nList[i]:nList[i+1]])
    return [Q,A]
def makeQuiz(path,quizName):
    strIn=""
    with open(path,'r') as fin:
      for line in fin:
        strIn+=line
        ########### we build Quiz() etc from strIn
    quiz=Quiz()
    quiz.quizName=quizName  
    x=findLatexBetween(r'{\\quiztype}{',strIn,'}')
    quizType=strIn[x[0][0]:x[0][1]]
    quiz.quizType=quizType                      
    x=findLatexBetween(r'\\begin{questions}',  #List of index pairs
                    strIn,r'\\end{questions}') #(start,stop)
    firstBatch=strIn[x[0][0]:x[0][1]]    #not sure I need x and y
    y=findLatexWhole(r'\\question ',firstBatch, r'\\end{choices}')
    quiz.Nquestions=len(y)#=number of questions in quiz    
    for i in range(quiz.Nquestions):                
        question=Question()
        question.QA=[]#I don't know why, but code ran without this
        question.Q=[]#ditto
        question.A=[]#ditto
        question.quizName=quizName
        question.number=i+1
        question.quizType=quizType
        #Now we search firstBatch for the i-th QA
        #(where QA is the "Question&Answers" string)
        questionAndAnswer=firstBatch[y[i][0]:y[i][1]]
        question.QA.append(questionAndAnswer)
        [Q0,A0]=QA2QandA(questionAndAnswer)
        question.Q.append(Q0)
        question.A.append(A0)
        #more needs to be added if numerical        
        #Also need questionQ an question.A
        quiz.questions.append(question)
    if question.quizType=="numerical":
        z=findLatexBetween(r'\\section{Renditions}',
                  strIn,r'\\section{Attribution}')        
        renditionsAll=strIn[z[0][0]:z[0][1]]
        w=findLatexWhole(r'\\begin{questions}',renditionsAll,
                         r'\\end{questions}')
        for i in range(quiz.Nquestions):
            #reopen each question from quiz
            question=quiz.questions[i]
            renditionsThis=renditionsAll[w[i][0]:w[i][1]]
            v=findLatexWhole(r'\\question ',renditionsThis,
                             r'\\end{choices}')
            question.Nrenditions=len(v)
            for j in range(len(v)):
                QAj=renditionsThis[v[j][0]:v[j][1]]
                 #########  fix pagebreak bug ################                             
                QAj=QAj.replace('\\pagebreak',' ')
                question.QA.append(QAj)
                question.Q.append(QAj)
                question.A.append(QAj)
    return quiz #ends MakeQuiz
def startLatex(titleTxt,quizType,statement,timeStamp): #this starts the Latex markup
    titleTex=titleTxt.replace('_','\\_')
    string=r'''%PREAMBLE
\newcommand{\quiztype}{'''+quizType+r'''}
\newif\ifkey\documentclass[11pt,twoside]{exam}
\RequirePackage{amssymb, amsfonts, amsmath, latexsym, verbatim,
xspace, setspace,datetime,tikz, pgflibraryplotmarks, hyperref}
\usepackage[left=.4in, right=.4in, bottom=.9in, top=.7in]{geometry}
\usepackage{endnotes, multicol,textgreek,graphicx} \singlespacing 
\parindent 0ex \hypersetup{ colorlinks=true, urlcolor=blue}
\pagestyle{headandfoot}
\runningheader{'''+titleTex+r'''}{\thepage\ of \numpages}{'''\
+timeStamp+r'''}
\footer{}
{\LARGE{The next page might contain more answer choices for this question}}{}
% BEGIN DOCUMENT 
\begin{document}\title{'''+titleTex+r'''}
\author{\includegraphics[width=0.10\textwidth]
{images/666px-Wikiversity-logo-en.png}\\
The LaTex code that creates this quiz is released to the Public Domain\\
Attribution for each question is documented in the Appendix\\
\url{https://bitbucket.org/Guy_vandegrift/qbwiki/wiki/Home}\\
\url{https://en.wikiversity.org/wiki/Quizbank} \\
'''+quizType+r''' quiz '''+timeStamp+\
r'''}
\maketitle
'''+statement+"\n\\tableofcontents\n"
    return string
def AllStudyLatex(testName,statement,timeStamp,bnkQuizzes):
    bigStrAll=startLatex(testName+": All","mixed",statement,timeStamp)
    bigStrStudy=startLatex(testName+":Study","mixed",statement,timeStamp)
    NbnkQuiz=len(bnkQuizzes)
    for i in range(NbnkQuiz):
        quiz=bnkQuizzes[i]
        quizNameLatex=prefixUnderscore(quiz.quizName)    
        bigStrAll+=r'\section{'+quizNameLatex\
            +r'}\keytrue\printanswers'
        bigStrStudy+=r'\section{'+quizNameLatex\
            +r'}\keytrue\printanswers'
        #print zeroth renditions
        bigStrAll+='\n\\begin{questions}\n'
        bigStrStudy+='\n\\begin{questions}\n' 
        for j in range(quiz.Nquestions):
            thisQA=quiz.questions[j].QA
            bigStrAll+=thisQA[0]
            bigStrStudy+=thisQA[0]
        bigStrAll+='\n\\end{questions}\n'
        bigStrStudy+='\n\\end{questions}\n'
        if(quiz.questions[j].quizType)=='numerical':
            bigStrAll+='\n \\subsection{Renditions}'     
            for j in range(quiz.Nquestions): #iterates questions not renditions 
                bigStrAll+='\n\\subsubsection*{'+quizNameLatex+' Q'+str(j+1)+'}'
                thisQA=quiz.questions[j].QA
                bigStrAll+='\n\\begin{questions}\n'            
                for k in range(1,len(thisQA)): #A null loop if conceptual
                    bigStrAll+=thisQA[k]
                bigStrAll+='\n\\end{questions}\n'
    bigStrAll+='\n\\section{Attribution}\\theendnotes\n\\end{document}'
    bigStrStudy+='\n\\section{Attribution}\\theendnotes\n\\end{document}'
    #Make output
    path2=os.path.join('.\output',timeName)
    if not os.path.exists(path2):
        os.makedirs(path2)
    pathAll=os.path.join('.\output',timeName+'All.tex')
    with open(pathAll,'w') as latexOut:
        latexOut.write(bigStrAll)
    pathStudy=os.path.join('.\output',timeName+'Study.tex')
    with open(pathStudy,'w') as latexOut:
        latexOut.write(bigStrStudy)
    return
def checkcsvData(csvData, bnkQuizzes):
    csvChecks=True
    csvNew=copy.deepcopy(csvData)
    NquestionsMax=0 #will grow to the most questions on any wikiquiz
    NquestionsOnTest=0 #This will sum up the selections column 3c
    NquestionsInBnk=0#number questions in (local) bank
    #skip the top row of csvDat for now:
    Nquizzes=len(bnkQuizzes)
    for i in range(Nquizzes):       
        #col 1a comments. No need to check (except for top row).
        #col 2b testname
        thing=bnkQuizzes[i].quizName
        ss=csvData[i+1][1]   
        if repr(thing)!=repr(ss):
            print(thing+', '+ss+',quizname mismatch fatal error')
            sys.exit()
            csvChecks=False
        #col 3c Sel=number selected for test. No need to check.
        NquestionsOnTest+=int(csvData[i+1][2])
        #col 4d number questions available from bank
        thing=bnkQuizzes[i].Nquestions
        NquestionsInBnk+=thing
        if thing>NquestionsMax:
            NquestionsMax=thing
        ss=csvData[i+1][3]
        if str(thing)!=str(ss):
            print('thing', repr(thing), 'ss', repr(ss))
            print(str(thing),ss,'available questions mismatch')
            csvNew[i+1][3]=thing
            csvChecks=False
        #col 5e t=type
        thing=bnkQuizzes[i].quizType
        ss=csvData[i+1][4]
        if thing=='conceptual':
            thing='c'
        if thing=='numerical':
            thing='n'
        if thing!=ss:
            print(thing,ss,'quiztype mismatch')
            csvNew[i+1][4]=str(thing)
            csvChecks=False
    #col 6f  1 (2,3,...) were obtained from the ss=spreadsheet
    #Now do the first row:  Now thing has to be calculated
    #col 1a thing is the number of questions on the Test
    ss=csvData[0][0] #col 1a has index 0
    thing=NquestionsOnTest
    if str(thing)!=str(ss):
        print(repr(thing),repr(ss),'Questions on Test mismatch')
        csvNew[0][0]=str(thing)
        csvChecks=False
    #ss=csvData[0][1] #col 2b is testName index 1
    ss=testName #here we disable the testName check
    thing=testName
    if str(thing)!=str(ss):
        print(thing,ss,'TestName mismatch')
        csvNew[0][1]=thing
        csvChecks=False
    ss=csvData[0][2] #col 3c Sel selection label index 2
    thing='Sel'
    if str(thing)!=str(ss):
        print(thing,ss,'Sel=selection col header not convention')
        csvNew[0][2]=thing
        csvChecks=False
    ss=csvData[0][3]
    thing=str(NquestionsInBnk)
    if str(thing)!=str(ss):
        print(thing,ss,'questions in (local) bank mismatch')
        csvNew[0][3]=str(thing)
        csvChecks=False
    ss=csvData[0][4] #col 5e is 't' type
    thing='t'
    if str(thing)!=str(ss):
        print(thing,ss,"needs 't' in top of column")
        csvNew[0][4]=thing
        csvChecks=False
    ss=csvData[0][5:len(csvData[0])]
    problemLabels=map(str,list(range(1,NquestionsMax+1)))
    thing=list(problemLabels)
    if str(thing)!=repr(ss)and str(thing)!=repr(ss[:-1]):
        print(thing,ss,'problemLabels mismatch')
        for n in range(0,NquestionsMax):
            csvNew[0][5+n]=str(n+1)
        csvChecks=False
    print('csvChecks =',csvChecks)
##    if csvChecks:
##        print('csv file seems correct')
##    else:
##        print('csv file needs UPGRADE - upgrading now')
##        upgradePath="./input/"+testName+'UPGRADE.csv'
##        with open(upgradePath,'w',newline='') as fout:
##            wr=csv.writer(fout,delimiter=',')
##            for row in csvNew:
##                wr.writerow(row)
    return csvChecks
def instructorLatex(testName,statement,timeStamp,bnkQuizzes,csvInfo):    
    bigStr=startLatex(testName+": Instructor","mixed",statement,timeStamp)
    bigStr+=r'\keytrue\printanswers'
    choiceList=[]
    for i in range(len(bnkQuizzes)):
        bigStr+=r'%New bank wikiquiz\\'
        bigStr+='\n'
        quiz=bnkQuizzes[i]
        quizName=quiz.quizName
        #must upgrade quizName to Latex format:
        quizName=quizName.replace('_','\\_')
        Nquestions=quiz.Nquestions        
        num2pick_str=csvInfo.selections[i+1]
        bigStr+=r'\subsection{'+num2pick_str
        bigStr+=' of '+ str(Nquestions) +' questions from '+quizName+'}\n'
        first=[]
        second=[]
        #choiceSel is the decision made regarding each question
        #in the wikiquiz: 0, 1, 2, or if outide range:''
        choiceSel=csvInfo.choices[i+1]
        #convert this list of "integer strings" to a string:
        bigStr+='\nSel: '
        bigStr+=', '.join(choiceSel)+r'.\\'
        iMax=len(choiceSel)
        for j in range(iMax):#note that the list starts at 1 not 0
            if choiceSel[j]=='1':
                first.append(int(j+1))
            if choiceSel[j]=='2':
                second.append(int(j+1))
        #randChoice selects randomly from this:
        randChoice=getFromChoices(first,second,int(num2pick_str))
        randChoice=sorted(randChoice)
        #create a printable string to show the random selection:
        bigStr+='\nRandom: '
        bigStr+=', '.join(map(str,randChoice))+r'.\\'+'\n'
        #choiceList is a list of lists:
        choiceList.append(randChoice)
        #all versions use these same choices and choice list saves
        #Review: i indexed the quizzes, so we let j index the questions

        #Begin fix to avoid void question gag
        if len(randChoice)!=0:
            bigStr+='\n'+'\\begin{questions}\n'
            for j in range(len(randChoice)):            
                jQ=randChoice[j]
                #print('randomChoice=jQ',jQ,'bnkquiz#=i',i)
                #recall that humans count questions beginning at 1
                #but python starts at zero:  jQ goes to jQ-1
                bigStr+=quiz.questions[jQ-1].QA[0]
            bigStr+='\n\\end{questions}\n'
        #End fix to avoid void gquestion tag
        
    bigStr+='\n\\section{Attribution}\n'
    bigStr+='Some attributions are located elsewhere.'
    bigStr+=r'''\endnote{The current system neglects to repeat the
attributions for the multipe renditions}'''
    bigStr+='\n\\theendnotes\n\\end{document}'        
    pathInstructor=os.path.join('.\output',timeName+'Instructor.tex')
    with open(pathInstructor,'w') as latexOut:
        latexOut.write(bigStr)
    return choiceList
def choiceList2string(choiceList,bnkQuizzes): #wherewasi
    #returns random selections from the choiceList in randomized order
    testQAlist=[]
    for i in range(len(bnkQuizzes)):
        quiz_i=bnkQuizzes[i]
        for j in range(len(choiceList[i])):
            choice_j=choiceList[i][j]
            jPy=choice_j-1#Python iterates from zero
            #choice_j is an integer
            question=quiz_i.questions[jPy]
            if question.quizType=="numerical":
                NR=question.Nrenditions
                nR=random.choice(range(1,NR))
                testQAlist.append(question.QA[nR])
            if question.quizType=="conceptual":
                Q=question.Q[0]
                Araw=question.A[0]
                Ashuffled=question.A[0]
                random.shuffle(Ashuffled)
                string=Q+'\\begin{choices}'
                for answer in Ashuffled:
                    string+=answer
                string+='\\end{choices}\n'
                #testQAlist.append(Q+Ashuffled)
                testQAlist.append(string)    
    random.shuffle(testQAlist)
    bigStr=''
    for item in testQAlist:
        bigStr+=item+'\n'
    return bigStr

def testLatex(testName,statement,timeStamp,bnkQuizzes,choiceList,
              nVersions):
    questionList=[]
    for n in range(len(bnkQuizzes)):
        quiz=bnkQuizzes[n]
        string=quiz.quizName+' ('+str(quiz.Nquestions)
        string+=' '+quiz.quizType+') choices: '
        string+=', '.join(map(str,choiceList[n]))
        #print(string)
        for quesNumStr in choiceList[n]:
            #pyQuesIndex gets us the question we want:
            pyQuesIndex=int(quesNumStr)-1
            question=quiz.questions[pyQuesIndex]
            questionList.append(question)
    bigStr=startLatex(testName+": Test","mixed",statement,timeStamp)
    #startLatex fixes the prefixUnderscore, but henceforth we need:
    testNameLatex=prefixUnderscore(testName)
    bigStr+='text before first subsection\n'
    for nver in range(nVersions):
        #noanswers part:
        bigStr+='\\cleardoublepage\\subsection{V'+str(nver+1)+'}\n'
        bigStr+='\\noprintanswers\\keyfalse\n'
        bigStr+='\\begin{questions}\n'
        testQAlist= choiceList2string(choiceList,bnkQuizzes)
        bigStr+=testQAlist       
        bigStr+='\\end{questions}\n'

        ##KEY part
        bigStr+='\\cleardoublepage\\subsubsection{KEY V'+str(nver+1)+'}\n'
        bigStr+='\\printanswers\\keyfalse\n'
        bigStr+='\\begin{questions}\n'
        bigStr+=testQAlist         
        bigStr+='\\end{questions}\n'    
    bigStr+='\n\\section{Attribution}\n'
    bigStr+='Some attributions are located elsewhere.'
    bigStr+=r'''\endnote{The current system neglects to repeat the
attributions for the multipe renditions}'''
    bigStr+='\n\\theendnotes\n\\end{document}'  
    pathTest=os.path.join('.\output',timeName+'Test.tex')
    with open(pathTest,'w') as latexOut:
        latexOut.write(bigStr)
    return 
####################   Program starts here ##########################
timeStamp=str(int(time.time()*100))
if debugmode==True:
    testName='s_ample'
    timeStamp='0000000' #and testName will be set to sample
    path2empty=os.path.join('.\output','0000000-sample')
    if os.path.exists(path2empty):
        shutil.rmtree(path2empty)
## This last line was an attempt to fix a mysterious failure.  I need
##        to remove path2empty because it is created later.
else:
    testName=getTestName()
##timeStamp=str(int(time.time()*100))
timeName = timeStamp+'-'+testName
path='./bank/'+testName+'.csv'
csvInfo=CsvInfo()
[csvInfo,cvsRows,csvData]=getCsv(csvInfo,testName)
#bnkQuizNames is a list of quznames in the bank
bnkQuizNames=csvInfo.quizNames[1:cvsRows]
bnkQuizzes=[]#bnkQuizzes is a list of objects of class Quiz()
for name in bnkQuizNames:
    if os. path. isfile('./bank/'+name+'.tex'):
        bnkQuizzes.append(makeQuiz('./bank/'+name+'.tex',name))
    else:
        print(name+'is not a quiz in the bank')
csvChecks=checkcsvData(csvData, bnkQuizzes)
AllStudyLatex(testName,statement,timeStamp,bnkQuizzes)
choiceList=instructorLatex(testName,statement,timeStamp,bnkQuizzes,csvInfo)
testLatex(testName,statement,timeStamp,bnkQuizzes,choiceList,
          Nversions)

NumDismantle.py edit

## NumDismantle dismantles a numerical wikiquiz. 

import os, sys, csv, re, time, copy, shutil, random#(re not used yet)

## These can be moved into NumDismantleAux using this:
##from NumDismantleAux import whatis,  Question, Quiz, prefixUnderscore, findLatexBetween,\
##     findLatexWhole, numericListGet, QA2QandA, makeQuiz, bankQuizHeader, bankQuizFooter,\
##     printQuiz
def whatis(string, x):
    print(string+' value=',repr(x),type(x))
    return string+' value='+repr(x)+repr(type(x))
###################################################### ENDS whatis ###
class Question:
    def __init__(self):
        self.quizName =""
        self.number=0 #
        self.quizType = ""
        self.QA = [] #question and answer
        self.Q=[] #question only (flawed for numericals?)
        self.A=[] #answer only (flawed for numericals?)
        self.Nrenditions = 1
###################################################### ends Question##        
class Quiz:
    def __init__(self):
        self.quizName =""
        self.quizType = ""
        self.questions = []#Question() clas list
        self.Nquestions=0#number of questions     
#############################################################ends Quiz
def prefixUnderscore(string):
    string=string.replace('_','\\_')
    return string
############################################## ends prefix underscore####
def findLatexBetween(pre,strIn,post):
    #returns list of indeces between the pre and post
    #tags.  Between excludes the tags
    mylist=[]
    tag=pre+r'(.*?)'+post
    matches=re.finditer(tag,strIn,re.S)
    for item in matches:
    #To get "between" we subtract out the pre and post
    #part of the tag.  We need to count the backslashes
    #in order to compensate for the extra \ in the \\ escape
        n1=item.start()+len(pre)-pre.count(r'\\')
        n2=item.end()-len(post)+post.count(r'\\')#as before:
    #mylist is a lists of two element lists (start/st
    #the two element list is the start/stop point in string
    #mylist is as long as there are instances of the tag
        mylist.append([n1,n2])
    return mylist                        
#################################################ends findLatexBetween
def findLatexWhole(pre,string,post):
    #returns list of indices between the pre and post
    #tags.  Whole includes the tags
    mylist=[]
    tag=pre+r'(.*?)'+post
    matches=re.finditer(tag,string,re.S)
    for item in matches:
        mylist.append([item.start(),item.end()])
    #mylist is a lists of two element lists (start/st
    #the two element list is the start/stop point in string
    #mylist is as long as there are instances of the tag
    return mylist  
################################################  ends findLatexWhole ###
def numericListGet(defaultQuiz,debugmode):
    Nnumerical=Nconceptual=Nunknown=0
    numericList=[]
    for item in os.listdir('./bank'):
        if item.endswith(".tex"):
            quizType=''
            sout=item
            with open('./bank/'+item,'r') as fin:
                strIn=fin.read()
                x=findLatexBetween(r'{\\quiztype}{',strIn,'}')
                quizType=strIn[x[0][0]:x[0][1]]
                if quizType=="numerical":
                    sout='n:'+sout
                    Nnumerical+=1
                    numericList.append(item[:-4])
                elif quizType=="conceptual":
                    sout='c:'+sout
                    Nconceptual+=1
                else:
                    sout='?: '
                    Nunknown+=1
    print('\ncounts: ',Nnumerical,'n ', Nconceptual,'c ',Nunknown,'?')
    numericList.append("Default")
    if debugmode==1:
        quizName=defaultQuiz
    else:
        print(numericList)
        quizName=input("Enter quiz: ")
        if quizName=="Default":
            quizName=defaultQuiz
    return quizName
################################################### ends numericListGet##
def QA2QandA(string): 
    #Define 4 patterns:
    beginStr=r'\\begin{choices}'
    endStr=r'\\end{choices}'
    CoStr=r'\\CorrectChoice '
    chStr=r'\\choice '
    #get Q=question
    n=string.find('\\begin{choices}')
    Q=string[0:n]
    #print('\nbeginQ\n',Q,'\endQ\n')#diagnostic
    #get A=string of answers
    #newStr defines the new search parameter as 1st answer
    pat=CoStr+'|'+chStr+'|'+endStr+'(.*?)'#fails-not greedy   
    iterations=re.finditer(pat,string,re.S)
    nList=[0]
    A=[]  
    for thing in iterations:
        nList.append(thing.start())
    #for i in range(len(nList)): #crashes as expected
    for i in range(1,len(nList)-1):
        A.append(string[nList[i]:nList[i+1]])
    return [Q,A]
#######################################################end QA2QandA ###
def makeQuiz(path,quizName):
    #This function has a flaw regarding questions/answers being different
    #for the first rendition.  It's ok as long as the first rendtion is
    #not used.  See the last few lines of makeQuiz.
    strIn=""
    with open(path,'r') as fin:
      for line in fin:
        strIn+=line
        ########### we build Quiz() etc from strIn
    quiz=Quiz()
    quiz.quizName=quizName  
    x=findLatexBetween(r'{\\quiztype}{',strIn,'}')
    quizType=strIn[x[0][0]:x[0][1]]
    quiz.quizType=quizType                      
    x=findLatexBetween(r'\\begin{questions}',  #List of index pairs
                    strIn,r'\\end{questions}') #(start,stop)
    firstBatch=strIn[x[0][0]:x[0][1]]    #not sure I need x and y
    y=findLatexWhole(r'\\question ',firstBatch, r'\\end{choices}')
    quiz.Nquestions=len(y)#=number of questions in quiz    
    for i in range(quiz.Nquestions):                
        question=Question()
        question.QA=[]#I don't know why, but code ran without this
        question.Q=[]#ditto
        question.A=[]#ditto
        question.quizName=quizName
        question.number=i+1
        question.quizType=quizType
        #Now we search firstBatch for the i-th QA
        #(where QA is the "Question&Answers" string)
        questionAndAnswer=firstBatch[y[i][0]:y[i][1]]
        question.QA.append(questionAndAnswer)
        [Q0,A0]=QA2QandA(questionAndAnswer)
        question.Q.append(Q0)
        question.A.append(A0)
        #more needs to be added if numerical        
        #Also need questionQ an question.A
        quiz.questions.append(question)
    if question.quizType=="numerical":
        z=findLatexBetween(r'\\section{Renditions}',
                  strIn,r'\\section{Attribution}')        
        renditionsAll=strIn[z[0][0]:z[0][1]]
        w=findLatexWhole(r'\\begin{questions}',renditionsAll,
                         r'\\end{questions}')
        for i in range(quiz.Nquestions):
            #reopen each question from quiz
            question=quiz.questions[i]
            renditionsThis=renditionsAll[w[i][0]:w[i][1]]
            v=findLatexWhole(r'\\question ',renditionsThis,
                             r'\\end{choices}')
            question.Nrenditions=len(v)
            for j in range(len(v)):
                QAj=renditionsThis[v[j][0]:v[j][1]]                            
                QAj=QAj.replace('\\pagebreak',' ')
                question.QA.append(QAj)
                #here is where the problem is.
                question.Q.append(QAj) 
                question.A.append(QAj)
    return quiz 
#################################################ends MakeQuiz
def bankQuizHeader(quizName):
    #requires prefixUnderscore
    sHeader =r'''
\newcommand{\quiztype}{numerical}%[[Category:QB/numerical]]
%%%%% PREAMBLE%%%%%%%%%%%%
\newif\ifkey %estabkishes Boolean ifkey to turn on and off endnotes
\documentclass[11pt]{exam}
\RequirePackage{amssymb, amsfonts, amsmath, latexsym, verbatim,
xspace, setspace,datetime}
\RequirePackage{tikz, pgflibraryplotmarks, hyperref}
\usepackage[left=.5in, right=.5in, bottom=.5in, top=.75in]{geometry}
\usepackage{endnotes, multicol,textgreek} %
\usepackage{graphicx} % 
\singlespacing %OR \onehalfspacing OR \doublespacing
\parindent 0ex % Turns off paragraph indentation
\hypersetup{ colorlinks=true, urlcolor=blue}
% BEGIN DOCUMENT 
\begin{document}'''+'\n'
    sHeader+='\\title{'+prefixUnderscore(quizName)+'}\n'
    sHeader+=r'''
\author{The LaTex code that creates this quiz is released to the Public Domain\\
Attribution for each question is documented in the Appendix}
\maketitle
\begin{center}                                                                                
 \includegraphics[width=0.15\textwidth]{images/666px-Wikiversity-logo-en.png}
\\For more information visit:\\
\footnotesize{
    \url{https://en.wikiversity.org/wiki/Quizbank}\\
    \url{https://bitbucket.org/Guy_vandegrift/qbwiki/wiki/Home}\\
    \url{https://en.wikiversity.org/wiki/special:permalink/1828921}}
\end{center}
\begin{frame}{}
\begin{multicols}{3}\tableofcontents\end{multicols}
\end{frame}
\pagebreak\section{Quiz}\keytrue\printanswers'''
    return sHeader    
################################################### ends bankQuizHeader
def bankQuizFooter():
    sFooter=r'''\pagebreak\section{Attribution}\theendnotes
\end{document}'''
    return sFooter
############################################### ends bankQuizFooter ########

    
def printQuiz(quizName,thisQuiz):
    theseQuestions=thisQuiz.questions
    folderName=timeStamp+'-'+quizName
    # first we make one folder with all questions
    sAll=''
    for thisQuestion in theseQuestions:
        sAll+='\n\\subsection{}\n\\begin{questions}\n'
        for rendition in thisQuestion.QA:
            sAll+=rendition
        sAll+='\n\\end{questions}\n'
    path2=os.path.join('.\output',folderName)
    if not os.path.exists(path2):
        os.makedirs(path2)
    pathAll=os.path.join('.\output',folderName+'_All.tex')
    with open(pathAll,'w') as fout:
        fout.write(bankQuizHeader(quizName))
        fout.write(sAll)
        fout.write(bankQuizFooter())
    # next we make folders for each question
    questionCount=countJumps[0] #iterates questioncount
    for thisQuestion in theseQuestions:
        sThis='\n\\subsection{}\n\\begin{questions}\n'
        for rendition in thisQuestion.QA:
            sThis+='\n'+rendition
        sThis+='\n\\end{questions}\n'
        sCount='_'+str(questionCount).zfill(3)+'.tex'
        pathThis=os.path.join('.\output',folderName+sCount)
        with open(pathThis,'w') as fout:
            fout.write(bankQuizHeader(quizName))
            fout.write(sThis)
            fout.write(bankQuizFooter())   
        questionCount+=countJumps[1]

############################################### ends bankQuizFooter ########
#
##      code starts here#      ###########
#defaultQuiz='SelectAll'
defaultQuiz='a07_energy_cart'
countJumps=(1,1) #(first, jump) in problem numbers
timeStamp=str(int(time.time()*100))
debugmode=0     #0 if not debugging
    #1: Use defaultQuiz.  Don't ask in numericListGet
quizName=numericListGet(defaultQuiz,debugmode)
thisQuiz=makeQuiz('./bank/'+quizName+'.tex',quizName)
printQuiz(quizName,thisQuiz)
print('Done')

MakeExcelTemplate.py edit

##Reads the tex file in ./bank, counts the questions and
##creates ./input/excelAll.cvs that can be saved as an excel
##file and used to create collections of quizzes for selection
##into exams.
 
##Code requires ./bank and ./input/template directories
import os, sys, csv, re #(re not used yet)

global DIAGNOSTIC
DIAGNOSTIC=True

def getNames(): #returns [quizNames,testName])
    testName="template"
    quizNames=[]
    availableTests=[]
    texset=set()
 #collect set of tex files in '/bank'        
    for item in os.listdir('./bank'): 
        if item.endswith('.tex'):
            texset.add(item[:-4])
            quizNames.append(item[:-4])
    print('Updating template.csv in ./input')
    print(len(quizNames),' bankquizzes are available for',testName)    
    return [quizNames,testName]
#end def getNames

def getQuizInfo(quizName):
    #returns the quiztype and number of questions
    path2=os.path.join('./bank',quizName+".tex")
    with open(path2,'r') as fin:
        quizText=fin.read()
    #get quizType (spelled quiztype in latex docs)    
    start=quizText.find(r'\quiztype')
    str2search=quizText[start+10:start+25]
    if str2search.find("conceptual")==1:
        quiztype="conceptual"
    elif str2search.find("numerical")==1:
        quiztype="numerical"
    else:
        quiztype="????????????????"
        print('unknown quiztype in getQuizInfo')
        sys.exit()
    #get numberOfQuestions (aka Nquestions)  
    start=quizText.find(r'\begin{questions}')
    stop=quizText.find(r'\end{questions}',start)
    str2search=quizText[start:stop]
    seeking=r'\question '
    numberOfQuestions=count_occurances(str2search,seeking)
    return[quiztype, numberOfQuestions]
# end def getQuizInfo

def find_all(a_str, sub): #this is a generator?
# x=list(find_all('spam spam spam spam', 'spam'))
#     print(x) yields: [0, 5, 10, 15]
    start = 0
    while True:
        start = a_str.find(sub, start)
        if start == -1: return
        yield start
        start += len(sub)
        # use start += 1 to find overlapping matches
# end def find_all

def count_occurances(a_str, sub):
# count how often "sub" exists in "a_str"
    return len(  list( find_all(a_str, sub) )  )
# end count_occurances

######################  START PROGRAM  ########################
[quizNames,testName]=getNames()  
for quizName in quizNames:
    ss=[]#start spreadsheet
    row=[]

#Row 1 colums: A=number questions in Test, B=TestName,
#C=Sel column: user-selected number to be on test
#D=local banksize E=quizType, F=first question    
    row.append("=sum(C2:C"+str(1+len(quizNames))+")") #A
    row.append("x-name") #testName                    #B
    row.append("Sel")#Select number questions  test   #C
    row.append("=sum(D2:D"+str(1+len(quizNames))+")") #D    
    row.append('t')  #quiz type                       #E
    row.append(1)    #first question                  #F
    string="=if(max(g2:g"+str(1+len(quizNames))\
               +")>0, F1+1,0)"#logical if 
    row.append(string)#allows fillright of top row    #G             
    ss.append(row)
    bankQuestionCount=0
    for quizName in quizNames:
        [quizType, Nquestions]=getQuizInfo(quizName)
        bankQuestionCount+=Nquestions
        row=[]
        row.append(" ") #blank workspace           #A
        row.append(quizName)                       #B
        row.append(1)                              #C
        row.append(Nquestions)                     #D
        if quizType=="conceptual":                 #E
            row.append("c")
        else:
            row.append("n")
        for j in range(Nquestions):                #F
            row.append("1")                
        ss.append(row)   
path2='./input/template/'+"templateXLXS"+'.csv'
with open(path2,'w',newline='') as fout:
    wr=csv.writer(fout,delimiter=',')
    for row in ss:
        wr.writerow(row)
print("The bank currently holds",bankQuestionCount,"questions")
print("Template update complete")
input('press enter to exit')

cleanup.py edit

#places all files into folder that starts with the same name
import os, shutil#(re not used yet)

def whatis(string, x):
    print(string+' value=',repr(x),type(x))
    return string+' value='+repr(x)+repr(type(x))
excluded=('images','archive','cleanup.py')
itemList=[]
for item in os.listdir():
    if item not in excluded:
        itemList.append(item)
folderList=[]
print('\n*folders:')
for item in filter(os.path.isdir, os.listdir(os.getcwd())):
    if item not in excluded:
        folderList.append(item)

for folder in folderList:
    print('\n*',folder,len(folder))
    for file in itemList:
        if file[:len(folder)]==folder and len(file)>len(folder):
            shutil.move(file,folder)

numMake.py edit

##NumMake finds a folder beginning that ends with _NUM in
##.\input and creates a numerical quiz with the same name in .\output


import os, time, sys, re
##from numMakeAux import whatis, prefixUnderscore, setup, makeHeader,\
##     makeFooter
##
##from numMakeAux import bankQuizPrint

################################### Recently moved from AUX file
def whatis(string, x):
    print(string+' value=',repr(x),type(x))
    return string+' value='+repr(x)+repr(type(x))
################################################# end whatis

def prefixUnderscore(string):
    string=string.replace('_','\\_')
    return string
################################################# end prefixUnderscore

def setup(debug,quizNameDefault):
    timeStamp=str(int(time.time()*100))          
    #1: used default NumFolder and default comment
    if debug==1:
        quizName=quizNameDefault
        numFolder=quizName+'_NUM'
        timeStamp='0000'
    else:
        numFolderList=[]
        #Search for *NUM files in input
        for item in os.listdir('../input'):
            if item.endswith("_NUM"):
                numFolderList.append(item)
        print(numFolderList)
        numFolder=input("select folder_NUM: ")
        quizName=numFolder[:-4]
    return [quizName,numFolder,timeStamp]
################################################# end setup

def makeHeader(quizName):
    #need to remove underscores in quizName
    quizname=prefixUnderscore(quizName)
    sHeader=r'\newcommand{\quizname}{'
    sHeader+=quizname+'}'
    sHeader+=r'''
%This numerical quiz was modified by merging in questions with numMake.py
\newcommand{\quiztype}{numerical}%[[Category:QB/numerical]]
%%%%% PREAMBLE  %%%%%%%
\newif\ifkey
%%%%% ifkey turns on/off endnotes
\documentclass[11pt]{exam}
\RequirePackage{amssymb, amsfonts, amsmath, latexsym, verbatim,
xspace, setspace,datetime}
\RequirePackage{tikz, pgflibraryplotmarks, hyperref}
\usepackage[left=.5in, right=.5in, bottom=.5in, top=.75in]{geometry}
\usepackage{endnotes, multicol} %try this }
\usepackage{graphicx}  
\singlespacing %OR \onehalfspacing OR \doublespacing
\parindent 0ex % Turns off paragraph indentation
\hypersetup{ colorlinks=true, urlcolor=blue}
%%%% BEGIN DOCUMENT 
\begin{document}
\title{\quizname}
\author{The LaTex code that creates this quiz is released to the Public Domain\\
Attribution for each question is documented in the Appendix}
\maketitle
\begin{center}
 \includegraphics[width=0.15\textwidth]{images/666px-Wikiversity-logo-en.png}
 \\Latex markup  at\\
 \footnotesize{
    \url{https://en.wikiversity.org/wiki/Quizbank}\\
    \url{https://bitbucket.org/Guy_vandegrift/qbwiki/wiki/Home}\\
    \url{https://en.wikiversity.org/wiki/special:permalink/1936879}}
\end{center}
\begin{frame}{}
  \begin{multicols}{3}\tableofcontents\end{multicols}
\end{frame}
\pagebreak\section{Quiz}
\keytrue\printanswers
'''
    return sHeader
################################################# end makeHeader

def makeFooter():
    return r'''\section{Attribution}\endnote{end}\theendnotes
\end{document}'''
################################################# end makeFooter
##########################################################################



###################################################  will be last 
def bankQuizPrint(timeStamp,quizName,sLatex):
    cwd=os.getcwd()
    path2QuizSoftware=os.path.dirname(cwd)#goes up
    path2output=os.path.join(path2QuizSoftware,'output')
    #This is the path to the timestamped folder
    path2new= os.path.join(path2output,timeStamp+'_'+quizName)
    if not os.path.exists(path2new):
        os.makedirs(path2new)
    #Write two identical Latex files with different names
    pathTest=os.path.join(path2output,timeStamp+'_'+quizName+'.tex')
    pathPost=os.path.join(path2new,quizName+'.tex')
    with open(pathTest,'w') as fout:
        fout.write(sLatex)
    with open(pathPost,'w') as fout:
        fout.write(sLatex)
    return


################################### Begin new procedures
def getProbs():
    path2probs=[]
    path1=os.getcwd() #path to numerical
    path2=os.path.dirname(path1) #path to QuizSoftwre
    path3=os.path.join(path2,'input')
    for item in os.listdir(path3):
    #There should be only one *_NUM folder, but 2B safe:
        if item.endswith('_NUM'):
            path4=os.path.join(path3,item)
            for item in os.listdir(path4):
                p2prob=os.path.join(path4,item)
                path2probs.append(p2prob)
    return path2probs

def findLatexWhole(pre,string,post):
    #returns list of indices between the pre and post
    #tags.  Whole includes the tags
    mylist=[]
    tag=pre+r'(.*?)'+post
    matches=re.finditer(tag,string,re.S)
    for item in matches:
        mylist.append([item.start(),item.end()])
    #mylist is a lists of two element lists (start/st
    #the two element list is the start/stop point in string
    #mylist is as long as there are instances of the tag
    return mylist

def printZeroRend():
    sLatex='\n\\begin{questions}\n'
    path2probs=getProbs()
    Nprob=len(path2probs)
    #allRend=[]
    for nprob in range(Nprob):

        path2=path2probs[nprob]
        with open(path2,'r') as fin:
            strIn=''
            for line in fin:
                strIn+=line
        x=findLatexWhole(r'\\question',  #List of index pairs
                        strIn,r'\\end{choices}') #(start,stop)    
        problem=strIn[x[0][0]:x[0][1]]
        sLatex+='\n'+problem+'\n'
    sLatex+='\n\\end{questions}\n'
    return(sLatex)

def printOtherRend():
    sLatex='\\newpage\\section{Renditions}\n'
    path2probs=getProbs()
    Nprob=len(path2probs)
    for nprob in range(0,Nprob):
        sLatex+='\n\\subsection{}%'+ str(nprob)+'\n'
        sLatex+='\\begin{questions}\n'

        path2=path2probs[nprob]
        with open(path2,'r') as fin:
            strIn=''
            for line in fin:
                strIn+=line
            x=findLatexWhole(r'\\question',  #List of index pairs
                        strIn,r'\\end{choices}') #(start,stop)
            #find number of renditions:
            Nrend=len(x)
            for nrend in range(1,Nrend):
                rendition=strIn[x[nrend][0]:x[nrend][1]]
                print('\nrendition\n', rendition)
                #strIn+='\n'+rendition+'\n'
                sLatex+='\n'+rendition+'\n'
                
            #sLatex+=strIn
        #sLatex+=strIn    #problem

        sLatex+='\\end{questions}\n'
    return sLatex

###############  start program #############################################    

debug=0
quizNameDefault='a07_energy_cart'
[quizName,numFolder,timeStamp]=setup(debug,quizNameDefault)
sLatex=makeHeader(quizName)
sLatex+='\n'
sLatex+=printZeroRend()
sLatex+='\n'
sLatex+=printOtherRend()
sLatex+=makeFooter()
bankQuizPrint(timeStamp,quizName,sLatex)
print('done')

copyAndRename.py edit

##Copy and rename this file so that newName.py will create
##newName.tex, which is a latex file with 20 renditions of
##the same numerical question with random variables.  This
##program must reside in the same program as DoNotEdit.py.
##newName.tex will not run properly without being in the
##same directory as the "images" folder.

import os, csv, re, time, shutil, random, sys, math
from DoNotEdit import roundSigFig, QuesVar, createFootnote,\
     startLatex, makeAnswers, finishLatex
random.seed() #needed to make random number different each time the code opens
def finishLatex():
    #Creates the latex .tex file
    firstRendition=True
    [questionString,prefix2question,answer2question,
     units4answer,insertImage,
     author,attribution,about]=writeQA(firstRendition)
    bs=startLatex(author,attribution, about)+'\n'+questionString
    firstRendition=False
    bs+='\n\\keytrue\n'
    for count in range(19):
        bs+='\n\\question\n'
        [questionString,prefix2question,answer2question,
         units4answer,insertImage,
         author,attribution,about]=writeQA(firstRendition)
        bs+=questionString
    bs+='\n\\end{questions}\n\\theendnotes\\end{document}'
    with open(os.path.basename(sys.argv[0][:-3])+'.tex','w') as fout:
        fout.write(bs)
    with open(os.path.basename(sys.argv[0][:-3])+'.txt','w') as fout:
        fout.write(bs)
############################################################################        
##########       AUTHOR OF NEW QUESTION STARTS HERE             ############        
############################################################################
def writeQA(firstRendition):    
    #Step 1: Fill in author, attribution, and short explanation   
    author=r'''OpenStax College University Physics'''
    attribution=r'''CC-BY copyright information available at \\
    \url{https://cnx.org/contents/1Q9uMg\_a@12.3:Gofkr9Oy@20/Preface}'''
    about=r'''We are grateful to David Marasco and Annie Chase
of Foothill College Physics. Their effort greatly facilitated
this attempt to turn the odd problems in OpenStax
University physics into a question bank that is also
an open educational resource'''
    
    #Step 2: To insert image, change False to True, add width and image name:
    insertImage=[False, 0.3,
         'Roller_coaster_energy_conservation.png']
    # The "images" folder must contain this image file

    #Step 3: Declare variables/image using the QuesVar class:
    # Reserved variable names:
    #   firstRendition  questionString  insertImage QuesVar
    #   prefix2answer   answer2question units2answer
    #   offByFactors    detractorsOffBy insertImage
    #   All QuesVar entities: *.t (text) and *.v (value)
       
    x1=   QuesVar(1, 2.1, 3, firstRendition)#first,min,max
    x2=   QuesVar(2, 4.1, 5, firstRendition)#
    x3=   QuesVar(3, 5.1, 6, firstRendition)#
         
    if insertImage[0]:   #This inserts image, but only if requested
        questionString='\\includegraphics[width='+str(insertImage[1])+\
           '\\textwidth]{images/'+insertImage[2]+r'}\\'+'\n'
        #the standard is for the first line to be the image
    else:
        questionString='' #Initializes question if no image is needed

    #Step 4: Write the question *.t is text, *.v is variable
    questionString+='''Alice has '''+x1.t+''' apples, '''\
    +x2.t+''' oranges and '''\
    +x3.t+''' pears. How many pieces of fruit does she have?'''    

    #Step 5: Solve the problem, defining (non-magic) variables as needed: 
    x=x1.v + x2.v # apples plus oranges
    y=x+x3.v # add the pears
   
    #Step 6: Use magic words to state answer, units (if needed)   
    prefix2answer=""# Or set to "" (empty string)
    answer2question = y
    units2answer =" pieces" #Blank to "" if dimensionless

    #Step 7 (optional): Adjust the wrong answers (detractors)
    #offByFactors is usually True: causing the  RATIO of two consecutive
    #detractors to equal "detractorsOffBy"
    offByFactors=True
    detractorsOffBy=1.1
    #If offByFactors is False, the DIFFERENCE between two consecutive 
    #detractors to equal "detractorsOffBy"

#########################################################################
############ There is no need to edit beyond this point           ######
#########################################################################    
    questionString+=createFootnote(author,attribution)
    questionString+=makeAnswers(prefix2answer, answer2question,
            units2answer,detractorsOffBy,offByFactors)
    return[questionString,prefix2answer,answer2question,
           units2answer,insertImage,author,attribution,about]
finishLatex() #This last dedented line is the program!

DoNotEdit.py edit

import os, csv, re, time, shutil, random, sys, math
def whatis(string, x):
    print(string+' value=',repr(x),type(x))
    return string+' value='+repr(x)+repr(type(x))
def roundSigFig(x, sigFig):
    #rounds positive or negative x to sigFig figures
    from math import log10, floor
    return round(x, sigFig-int(floor(log10(abs(x))))-1)
class QuesVar:
    #creates random input variables x.v(t) is number(text)
    def __init__(self, first, low, high, firstRendition):
        sigFigQues=3
        self.precision=3 #Number of digits to round
        self.first=first
        # see if either low or high are floating
        oneFloats=isinstance(low,float) or isinstance(high,float)
        if firstRendition:
            temp=first #temp is the local variable for self.v
            self.v=temp #v=value
        else:  #it is not the first question and may or may not float
            if oneFloats: #select a random floating value
                temp=random.uniform(low,high)
                temp=roundSigFig(temp,sigFigQues)
                self.v=temp
            else:
                temp=random.randint(low,high)
                temp=roundSigFig(temp,sigFigQues)
                self.v=temp
        #tempV=self.v and is used to create self.t (text version
        if abs(temp) > .01 and abs(temp) < 99:
            self.t=str(temp)
        else:
            formatStatement='{0:1.'+str(sigFigQues)+'E}'
            self.t=formatStatement.format(temp)
def createFootnote(author,attribution):
    s='\n\\ifkey\\endnote{Question licensed by '
    s+=author+' under Creative Commons '+attribution
    s+=r'}\else{}\fi'
    return s
def makeAnswers(prefix2answer, answer2question,
            units2answer,detractorsOffBy,offByFactors):
    sigFigAns=2
    s4mat="{:."+str(sigFigAns)+"E}" #string format
    offBy=detractorsOffBy #separation between answers
    nCorrect=random.randint(0,4)
    s='\n\\begin{choices}\n'
    for n in range(5):
        if n==nCorrect:           
            s+='\\CorrectChoice '+prefix2answer
            s+=s4mat.format(answer2question)
            s+=units2answer+'\n'
        else:
            s+='\\choice '+prefix2answer
            if not offByFactors:
                error=offBy*(n-nCorrect)
                thisAnswer=answer2question+error
                s+=s4mat.format(thisAnswer)
                s+=units2answer+'\n'
            if offByFactors:
                error=offBy**(n-nCorrect)
                thisAnswer=answer2question*error
                s+=s4mat.format(thisAnswer)
                s+=units2answer+'\n'              
    s+='\\end{choices}\n'
    return s
def finishLatex():
    #Creates the latex .tex file
    firstRendition=True
    [questionString,prefix2question,answer2question,
     units4answer,insertImage,
     author,attribution,about]=writeQA(firstRendition)
    bs=startLatex(author,attribution, about)+'\n'+questionString
    firstRendition=False
    bs+='\n'
    for count in range(19):
        bs+='\n\\question\n'
        [questionString,prefix2question,answer2question,
         units4answer,insertImage,
         author,attribution,about]=writeQA(firstRendition)
        bs+=questionString
    bs+='\n\\end{questions}\n\\theendnotes\\end{document}'
    with open(os.path.basename(sys.argv[0][:-3])+'.tex','w') as fout:
        fout.write(bs)
    with open(os.path.basename(sys.argv[0][:-3])+'.txt','w') as fout:
        fout.write(bs)
def startLatex(author, attribution, about):    
    nameQuestion=os.path.basename(sys.argv[0][:-3])
    bs=r'\newcommand{\nameQuestion}{'+nameQuestion+'}\n'
    bs+=r'''\newcommand{\quiztype}{numerical}
\newif\ifkey \keytrue \documentclass[11pt]{exam}
\RequirePackage{amssymb, amsfonts, amsmath, latexsym, verbatim,xspace, setspace,datetime}
\RequirePackage{tikz, pgflibraryplotmarks, hyperref,textcomp}
\usepackage[left=.5in, right=.5in, bottom=.5in, top=.75in]{geometry}
\usepackage{endnotes, multicol,textgreek} \usepackage{graphicx} 
\singlespacing  \parindent 0ex \hypersetup{ colorlinks=true, urlcolor=blue}
\begin{document}\title{\nameQuestion}
\author{Question author: '''+author+r''' \\
''' + 'License: ' + attribution+r'''\\
LaTex code that generate this question is released to the Public Domain}
''' + r'''\maketitle\begin{center}                                                                                
\includegraphics[width=0.15\textwidth]{images/666px-Wikiversity-logo-en.png}
\\For more information visit:\\
\footnotesize{
    \url{https://en.wikiversity.org/wiki/Quizbank}\\
    \url{https://bitbucket.org/Guy_vandegrift/qbwiki/wiki/Home}}
\end{center}'''
    bs+='\n'+about+'\n'
    bs+='\\printanswers\\begin{questions}\n\\question'
    return bs

def makeAnswers(prefix2answer, answer2question,
            units2answer,detractorsOffBy,offByFactors):
    sigFigAns=2
    s4mat="{:."+str(sigFigAns)+"E}" #string format
    offBy=detractorsOffBy #separation between answers
    nCorrect=random.randint(0,4)
    s='\n\\begin{choices}\n'
    for n in range(5):
        if n==nCorrect:           
            s+='\\CorrectChoice '+prefix2answer
            s+=s4mat.format(answer2question)
            s+=units2answer+'\n'
        else:
            s+='\\choice '+prefix2answer
            if not offByFactors:
                error=offBy*(n-nCorrect)
                thisAnswer=answer2question+error
                s+=s4mat.format(thisAnswer)
                s+=units2answer+'\n'
            if offByFactors:
                error=offBy**(n-nCorrect)
                thisAnswer=answer2question*error
                s+=s4mat.format(thisAnswer)
                s+=units2answer+'\n'              
    s+='\\end{choices}\n'
    return s

Footnotes edit