From e1b235f16a048dae47fb0a8bb9f94e25b0c05939 Mon Sep 17 00:00:00 2001
From: Mathieu Loiseau <mathieu.loiseau@univ-grenoble-alpes.fr>
Date: Wed, 26 Jan 2022 18:33:35 +0100
Subject: [PATCH] Cloze type questions

---
 XML_Moodle.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++----
 utils.py      |  4 ++--
 2 files changed, 63 insertions(+), 7 deletions(-)

diff --git a/XML_Moodle.py b/XML_Moodle.py
index b96ba3b..24d1892 100755
--- a/XML_Moodle.py
+++ b/XML_Moodle.py
@@ -1,9 +1,11 @@
 #!/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):
@@ -27,6 +29,8 @@ class Quizz:
 			newQ = ShortAnswer(q,self.categories[c],len(self.questions[c]),self.folder)
 		elif q.attrib['type'] == "truefalse":
 			newQ = TF(q,self.categories[c],len(self.questions[c]),self.folder)
+		elif q.attrib['type'] == "cloze":
+			newQ = Cloze(q, self.categories[c], len(self.questions[c]), self.folder)
 		elif q.attrib['type'] == "matching":
 			newQ = Question(q,self.categories[c],len(self.questions[c]),self.folder)#non traité
 		elif q.attrib['type'] == "ddimageortext":
@@ -65,11 +69,12 @@ class Quizz:
 class Question:
 	def __init__(self,xmlQ,c,n,f):
 		self.folder = f
-		self.id = c+"."+mlang_2_multiling(xmlQ.find("name/text").text,"en")+f".{n}"
+		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.category = c
 		self.env = "todo:"+xmlQ.attrib["type"]
-		self.max = float(xmlQ.find("defaultgrade").text)
+		if xmlQ.find("defaultgrade") != None:
+			self.max = float(xmlQ.find("defaultgrade").text)
 		self.parseImages(xmlQ.findall(".//file"))
 
 	def parseImages(self, file_elements):
@@ -83,11 +88,62 @@ class Question:
 		#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']
+	def __init__(self, xmlQ,c,n,f):
+		super().__init__(xmlQ,c,n,f)
+		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)
+		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] =="øØ":
+				scores = c.split("øØ")
+				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(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):
+	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.points = round((float(b)/100)*float(max),3)
 		self.feedback = fb
 
 	def __str__(self):
@@ -187,7 +243,7 @@ class ShortAnswer(TF):
 		return res
 
 if __name__ == "__main__":
-	quizz = Quizz("data/SID questions.xml", "data")
+	quizz = Quizz("data/KNM questions.xml", "data")
 	#quizz = Quizz("data/quiz-GI-4.xml" , "data")
 	#print(quizz)
 	quizz.save()
diff --git a/utils.py b/utils.py
index dd896b6..9d1d926 100644
--- a/utils.py
+++ b/utils.py
@@ -22,7 +22,7 @@ def process_listings(txt, folder="",q="q"):
 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>","}")
+	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>","}")
 	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
@@ -43,5 +43,5 @@ def score_2_str(v):
 	if int(v)==float(v):
 		res = str(int(v))
 	else:
-		res = str(float(v))
+		res = str(round(float(v),3))
 	return res
-- 
GitLab