diff --git a/XML_Moodle.py b/XML_Moodle.py index bbf757e997e7622705404a1ef6c74872010aadaa..fc86b8c517712fda5bdfd08768e17c26735ba4ed 100755 --- a/XML_Moodle.py +++ b/XML_Moodle.py @@ -8,12 +8,14 @@ from io import BytesIO from re import findall,DOTALL class Quizz: - def __init__(self, file_name, target_folder): + 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): @@ -24,17 +26,17 @@ class Quizz: 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) + 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) + 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) + 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) + 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)#non traité + 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)#non traité + 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": @@ -67,10 +69,12 @@ class Quizz: output.write(self.__str__()) class Question: - def __init__(self,xmlQ,c,n,f): + 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 = strip_tags(mlang_2_multiling(xmlQ.find("questiontext/text").text), self.folder,self.id) + self.q = self.strip_tags(xmlQ.find("questiontext/text").text) self.category = c self.env = "todo:"+xmlQ.attrib["type"] if xmlQ.find("defaultgrade") != None: @@ -82,7 +86,10 @@ class Question: name = i.attrib["name"] im = Image.open(BytesIO(base64.b64decode(i.text))) ext = name[name.rfind('.')+1:].upper() - im.save(self.folder+"/Images/"+name, ext) + 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" @@ -90,14 +97,15 @@ class Question: 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'] - def __init__(self, xmlQ,c,n,f): - super().__init__(xmlQ,c,n,f) + 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 = strip_tags(mlang_2_multiling(xmlQ.find("questiontext/text").text.replace("%","øØ")), self.folder,self.id) + self.q = self.strip_tags(xmlQ.find("questiontext/text").text.replace("%",Cloze.PC)) if self.__class__.__name__ == "Cloze": self.__parseAnswers(self.q) @@ -117,8 +125,8 @@ class Cloze(Question): self.choices_num[l] += 1 if c[0] == "=": self.choices.append(Answer(f"{l}) {c[1:]}", 100/n, self.max)) - elif c[0:2] =="øØ": - scores = c.split("øØ") + 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)) @@ -166,8 +174,8 @@ class Answer: class MCQ(Question): HORIZONTAL = 1 VERTICAL = 0 - def __init__(self, xmlQ,c,n,f): - super().__init__(xmlQ,c,n,f) + 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" @@ -179,8 +187,8 @@ class MCQ(Question): for a in xmlQ.findall("answer"): fb = a.find("feedback/text").text if fb != None: - fb = strip_tags(mlang_2_multiling(fb), self.folder, self.id) - self.choices.append(Answer(strip_tags(mlang_2_multiling(a.find("text").text), self.folder,self.id), a.attrib['fraction'], self.max, fb)) + 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: @@ -201,8 +209,8 @@ class MCQ(Question): return res class TF(MCQ): - def __init__(self, xmlQ,c,n,f): - super().__init__(xmlQ,c,n,f) + 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 @@ -213,15 +221,15 @@ class TF(MCQ): for a in xmlQ.findall("answer"): fb = a.find("feedback/text").text if fb != None: - fb = strip_tags(mlang_2_multiling(fb), self.folder, self.id) + 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,l=2): - super().__init__(xmlQ,c,n,f) + 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) diff --git a/mood2amc.py b/mood2amc.py new file mode 100755 index 0000000000000000000000000000000000000000..b29cd506226c80cfb36b9602151d058d8c61d302 --- /dev/null +++ b/mood2amc.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import argparse +from argparse import RawTextHelpFormatter #pour le formattage de l'aide +#déclaration des arguments +parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter,description="""Convertir un export de base de questions moodle en sujet AMC +\033[1m\033[32mex :\033[0m +‣\033[0m\033[32m./mood2amc.py -m base-de-questions_moodle.xml\033[0m +‣\033[0m\033[32m./mood2amc.py -u -m data/base-de-questions_moodle.xml -d data -s images\033[0m +‣\033[0m\033[32m./mood2amc.py -m bdm.xml -d /home/user/Documents/Exams/mon_super_exam \033[0m""") +parser.add_argument("-m","--moodledb", help="la base de questions moodle xml à convertir", type=str, default=None) +parser.add_argument("-u","--unsafe", help="convertir les <span> monospace en listings (dangereux)", action="store_true") +parser.add_argument("-d","--destination", help="dossier de destination", type=str, default="data") +parser.add_argument("-s","--sub_dir", help="Dossier où mettre les images et listings (sous-dossier de destination)", type=str, default="images") + +config = vars(parser.parse_args()) +from os import path, makedirs +if not path.isdir(config["destination"]): + makedirs(config["destination"],0o755) +if config["destination"][-1] != "/": + config["destination"]+="/" +if config["sub_dir"][-1] != "/": + config["sub_dir"]+="/" +if not path.isdir(config["destination"]+config["sub_dir"]): + makedirs(config["destination"]+config["sub_dir"],0o755) + +from XML_Moodle import Quizz +Quizz(config["moodledb"], config["destination"], config["sub_dir"], config["unsafe"]).save() diff --git a/utils.py b/utils.py index 9d1d9262155c62beebd7041f81c85a52ca5fb8ea..adfb574a3ef9b4923d7c2b29c10f266d0e4ce238 100644 --- a/utils.py +++ b/utils.py @@ -3,32 +3,33 @@ from re import compile,sub,escape,findall,DOTALL from html import unescape from urllib.parse import unquote -unsafe = True - -def process_listings(txt, folder="",q="q"): +def process_listings(txt, folder,subf, q): txt = txt.replace("\r\n","\n") listings = findall(r'\<pre\>(.*)\</pre\>', txt, DOTALL) i=0 for l in listings: - if folder != "": - folder+="/" i+=1 - filename = folder+"Images/"+q+str(i)+".txt" + filename = folder+subf+q+str(i)+".txt" f = open(filename, "w") f.write(unescape(l)) txt = txt.replace("<pre>"+l+"</pre>","\t\t\\lstinputlisting[language=Python]{"+filename.replace(folder,"")+"}") return txt -def remove_moodle_cdata(txt, folder, q): - global unsafe - txt = process_listings(txt, folder, q) - res = sub(r'<img src="[^/]*/([^"]*)" (alt="([^"]*)")?[^>]*>', r"""\\\\\\includegraphics[width=0.8\\linewidth]{Images/\1}""",txt).replace("<![CDATA[","").replace("]]>","").replace("<strong>","\\emph{").replace("</strong>","}").replace("<em>","\\emph{").replace("</em>","}") +def preprocess_images(txt, folder, subf, q): + #"$£a1@ø" inserted by regexp, to be replaced by variable (dirty) + txt = sub(r'<img src="[^/]*/([^"]*)" (alt="([^"]*)")?[^>]*>', r"\\\\\\includegraphics[width=0.8\\linewidth]{$£a1@ø\1}",txt).replace("$£a1@ø", subf) + return txt + +def remove_moodle_cdata(txt, folder, subf, unsafe, q): + txt = process_listings(txt.replace("<![CDATA[", "").replace("]]>", "").replace("<strong>", "\\emph{").replace("</strong>", "}").replace("<em>", "\\emph{").replace("</em>", "}"), + folder, subf, q) + txt = preprocess_images(txt, folder, subf, q) if unsafe: - res = res.replace('<span style="font:monospace">',"\lstinline[language=python]|").replace('<span style="font-family:monospace">',"\lstinline[language=python]|").replace('<span style="font-family=monospace">',"\lstinline[language=python]|").replace("</span>","|") - return res + txt = txt.replace('<span style="font:monospace">',"\lstinline[language=python]|").replace('<span style="font-family:monospace">',"\lstinline[language=python]|").replace('<span style="font-family=monospace">',"\lstinline[language=python]|").replace("</span>","|") + return txt -def strip_tags(txt, folder,q="q"): - return unescape(sub(compile('<.*?>'), '',remove_moodle_cdata(unquote(txt.replace(" ","~")),folder,q))) +def strip_tags(txt, folder, subf, unsafe, q="q"): + return unescape(sub(compile('<.*?>'), '',remove_moodle_cdata(unquote(txt),folder,subf, unsafe, q))) def mlang_2_multiling(txt, which = "both"): if which == "fr":