#!/usr/bin/env python3 import xml.etree.ElementTree as ET from unidecode import unidecode from utils import strip_tags, mlang_2_multiling, score_2_str import base64 from PIL import Image from io import BytesIO from re import findall,DOTALL class Quizz: def __init__(self, file_name, target_folder, extrafiles_folder, risky): self.file_name = file_name self.tree = ET.parse(self.file_name) self.questions = {} self.categories = {} self.folder = target_folder self.xtra_f = extrafiles_folder self.risky = risky self.parse() def get_tree(self): return self.tree def create_question(self, q, c): if c not in self.questions.keys(): self.questions[c] = [] self.categories[c] = f"c{len(self.categories)}" if q.attrib['type'] == "multichoice": newQ = MCQ(q,self.categories[c],len(self.questions[c]),self.folder,self.xtra_f, self.risky) elif q.attrib['type'] == "shortanswer": newQ = ShortAnswer(q,self.categories[c],len(self.questions[c]),self.folder, self.xtra_f, self.risky) elif q.attrib['type'] == "truefalse": newQ = TF(q,self.categories[c],len(self.questions[c]),self.folder, self.xtra_f, self.risky) elif q.attrib['type'] == "cloze": newQ = Cloze(q, self.categories[c], len(self.questions[c]), self.folder, self.xtra_f, self.risky) elif q.attrib['type'] == "matching": newQ = Question(q,self.categories[c],len(self.questions[c]),self.folder, self.xtra_f, self.risky)#non traité elif q.attrib['type'] == "ddimageortext": newQ = Question(q,self.categories[c],len(self.questions[c]),self.folder, self.xtra_f, self.risky)#non traité else: raise f"{q.attrib['type']} is not expected" if newQ.__class__.__name__ != "Question": self.questions[c].append(newQ) def parse(self): questions = self.tree.findall("question") for q in questions: if q.attrib["type"] == "category": c = q.find("category/text").text.replace("$course$/top/","").replace("/",":").replace("::","/") else: self.create_question(q, c) def __str__(self): res = "" digest ="%%%Exemple d'examen\n\t\t%\\cleargroup{exam}\n" for c,c_q in self.questions.items(): tmp = "\t\t%\\shufflegroup{"+self.categories[c]+"}%("+c+"):" count = 0 for q in c_q: res += str(q) count += 1 if count > 0: digest += f"{tmp}{count} questions\n\t\t%\\copygroup[{int(count / 2)}]" + "{" + self.categories[c] + "}" + "{exam}\n" return digest+"\t\t%\shufflegroup{exam}\n\t\t%\insertgroup{exam}\n\n"+res def save(self): name = self.file_name[self.file_name.rfind('/')+1:self.file_name.rfind('.')] output = open(self.folder+"/"+name+".tex", "w") output.write(self.__str__()) class Question: def __init__(self,xmlQ,c,n,f,x,r=False): self.folder = f self.xtra_f = x self.risky = r self.id = unidecode(c+"."+mlang_2_multiling(xmlQ.find("name/text").text.replace(" ",""),"en")+f".{n}") self.q = self.strip_tags(xmlQ.find("questiontext/text").text) self.category = c self.env = "todo:"+xmlQ.attrib["type"] if xmlQ.find("defaultgrade") != None: self.max = float(xmlQ.find("defaultgrade").text) self.parseImages(xmlQ.findall(".//file")) def parseImages(self, file_elements): for i in file_elements: name = i.attrib["name"] im = Image.open(BytesIO(base64.b64decode(i.text))) ext = name[name.rfind('.')+1:].upper() im.save(self.folder+self.xtra_f+name, ext) def strip_tags(self, txt): return strip_tags(mlang_2_multiling(txt), self.folder, self.xtra_f, self.risky, self.id) def __str__(self): #return "\\element{" + self.category + "}{\n\t\\begin{" + self.env + "}{" + self.id + "}\\nbpoints{" + score_2_str(self.max) + "}\n\t\t" + self.q + "\\end{" + self.env + "}\n" return "" class Cloze(Question): zones = ['A', 'B', 'C', 'D', 'E', 'F','G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] PC = "øØ" #character that replaces "%" (cf. url decode pb) def __init__(self, xmlQ,c,n,f,x,r): super().__init__(xmlQ,c,n,f,x,r) self.max = float(xmlQ.find("penalty").text) self.choices = [] self.choices_num = {} self.choice_type = MCQ.VERTICAL self.env = "questionmult" self.q = self.strip_tags(xmlQ.find("questiontext/text").text.replace("%",Cloze.PC)) if self.__class__.__name__ == "Cloze": self.__parseAnswers(self.q) def __parseAnswers(self, txt): questions = findall(r'\{:MCS?:([^\}]*)\}', txt, DOTALL) i = 0 for c in questions: self.create_choices(c, Cloze.zones[i], len(questions)) txt = txt.replace(r"{:MCS:"+c+"}", "\\underline{~~~~~~~~}("+Cloze.zones[i]+")").replace(r"{:MC:"+c+"}", "\\underline{~~~~~~~~}("+Cloze.zones[i]+")") i += 1 self.q = txt def create_choices(self, c_list, l, n): self.choices_num[l] = 0 choices = c_list.split("~") for c in choices: self.choices_num[l] += 1 if c[0] == "=": self.choices.append(Answer(f"{l}) {c[1:]}", 100/n, self.max)) elif c[0:2] == Cloze.PC: scores = c.split(Cloze.PC) self.choices.append(Answer(f"{l}) {scores[2]}", float(scores[1]), self.max)) else : self.choices.append(Answer(f"{l}) {c}", 0, self.max)) def __str__(self): res = "\\element{" + self.category + "}{\n\t\\begin{" + self.env + "}{" + self.id + "}\\nbpoints{" + score_2_str(self.max) + "}\\\\\n" + self.q +"\n\t\t\\begin{multicols}{"+str(max(2,len(self.choices_num)))+"}\n\t\t\t\\begin{choices}[o]\n\\AMCnoCompleteMulti" res += "\n" l = 0 i = 0 for c in self.choices: i += 1 res += str(c) if self.choices_num[Cloze.zones[l]] == i: i = 0 l += 1 if l < len(self.choices_num): res += "\\vfill\\null\n\\columnbreak\n" res += "\n\t\t\t\\end{choices} \n\t\t\\end{multicols} \n\t\\end{" + self.env + "}\n}\n\n" return res class Answer: def __init__(self,t,b,max,fb=None): self.status = float(b) > 0 self.text = t self.points = round((float(b)/100)*float(max),3) self.feedback = fb def __str__(self): res = "\t\t\t" if self.status: res += "\\correctchoice{" else: res += "\\wrongchoice{" res += self.text res+="}" if self.points >= 0: res += "\\bareme{b="+str(self.points)+"}" else: res += "\\bareme{m="+str(self.points)+"}" if self.feedback != None: res += "\n\\explain{"+self.feedback+"}" return res+"\n" class MCQ(Question): HORIZONTAL = 1 VERTICAL = 0 def __init__(self, xmlQ,c,n,f,x,r): super().__init__(xmlQ,c,n,f,x,r) self.choices = [] self.choice_type = MCQ.VERTICAL self.env = "questionmult" if self.__class__.__name__ == "MCQ": self.__parseAnswers(xmlQ) def __parseAnswers(self, xmlQ): self.shuffle = xmlQ.find("shuffleanswers").text == "true" for a in xmlQ.findall("answer"): fb = a.find("feedback/text").text if fb != None: fb = self.strip_tags(mlang_2_multiling(fb)) self.choices.append(Answer(self.strip_tags(a.find("text").text), a.attrib['fraction'], self.max, fb)) def get_choice_env(self): if self.choice_type == MCQ.VERTICAL: return "choices" else: return "choiceshoriz" def __str__(self): res = """\\element{"""+self.category+"""}{ \\begin{"""+self.env+"""}{"""+self.id+"""}\\nbpoints{"""+score_2_str(self.max)+"""}\\\\ """+self.q+"\n\t\t\\begin{"+self.get_choice_env()+"}" if not self.shuffle: res += "[o]" res += "\n" for c in self.choices: res += str(c) res += "\t\t\\end{"+self.get_choice_env()+"}\n\t\\end{"+self.env+"}\n}\n\n" return res class TF(MCQ): def __init__(self, xmlQ,c,n,f,x,r): super().__init__(xmlQ,c,n,f,x,r) self.choices = [] self.env = "question" self.shuffle = False if self.__class__.__name__ == "TF": self.__parseAnswers(xmlQ) def __parseAnswers(self, xmlQ): for a in xmlQ.findall("answer"): fb = a.find("feedback/text").text if fb != None: fb = self.strip_tags(mlang_2_multiling(fb)) if a.find("text").text == "true": self.choices.append(Answer("\\multiling{vrai}{true}", a.attrib['fraction'], self.max, fb)) else: self.choices.append(Answer("\\multiling{faux}{false}", a.attrib['fraction'], self.max, fb)) class ShortAnswer(TF): def __init__(self, xmlQ,c,n,f,x,r,l=2): super().__init__(xmlQ,c,n,f,x,r) self.nb_lines = l if self.__class__.__name__ == "ShortAnswer": self.__parseAnswers(xmlQ) def __parseAnswers(self, xmlQ): note = 0 while note < self.max: self.choices.append(note) note += 0.25 def __str__(self): res = """\\element{"""+self.category+"""}{ \\begin{"""+self.env+"""}{"""+self.id+"""}\\nbpoints{"""+score_2_str(self.max)+"""}\\\\ """+self.q+"\n\t\t\\AMCOpen{lineheight=0.6cm,lines="+str(self.nb_lines)+"}{" for c in self.choices: res += "\\wrongchoice{"+score_2_str(c)+"}\\scoring{"+score_2_str(c)+"}" res += "\\correctchoice{"+score_2_str(self.max)+"}\\scoring{"+score_2_str(self.max)+"}" res += "}\n\t\\end{"+self.env+"}\n}\n\n" return res if __name__ == "__main__": quizz = Quizz("data/KNM questions.xml", "data") #quizz = Quizz("data/quiz-GI-4.xml" , "data") #print(quizz) quizz.save()