from pandas import read_csv, read_excel, isna, DataFrame
from warnings import catch_warnings, simplefilter
from configparser import ConfigParser
from PIL import Image
from base64 import b64encode
from pathlib import Path
import tempfile, os
from string import punctuation
from pymoodef.common import _string_to_vector, _is_numeric_answer, _has_gaps
from pymoodef.numerical import _generate_numerical
from pymoodef.shortanswer import _generate_shortanswer
from pymoodef.multichoice import _generate_multichoice
from pymoodef.ordering import _generate_ordering
from pymoodef.ddwtos import _generate_ddwtos
from pymoodef.gapselect import _generate_gapselect
from pymoodef.matching import _generate_matching
from pymoodef.essay import _generate_essay
from pymoodef.truefalse import _generate_truefalse
[docs]
class Questions:
"""Defines a set of questions to be included in the Moodle question bank."""
def __init__(self):
"""Initialize attributes."""
self.__category = 'Default category'
self.__first_question_number = '1'
self.__copyright = ''
self.__license = ''
self.__correct_feedback = 'Correct.'
self.__partially_correct_feedback = 'Partially correct.'
self.__incorrect_feedback = 'Incorrect.'
self.__adapt_images = 'false'
self.__width = '800'
self.__height = '600'
self.__questions = None
self.__path = ''
[docs]
def define_ini(self, file):
"""Define configuration values.
These values are associated with each defined question.
Parameters
----------
file : str
Path to ini file.
Returns
-------
None
Examples
--------
>>> q = Questions()
>>> q.define_ini('tests/questions.ini')
"""
config = ConfigParser()
config.read(file)
if 'category' in config['DEFAULT']:
self.__category = config['DEFAULT']['category']
if 'first_question_number' in config['DEFAULT']:
self.__first_question_number = config['DEFAULT']['first_question_number']
if 'copyright' in config['DEFAULT']:
self.__copyright = config['DEFAULT']['copyright']
if 'license' in config['DEFAULT']:
self.__license = config['DEFAULT']['license']
if 'correct_feedback' in config['DEFAULT']:
self.__correct_feedback = config['DEFAULT']['correct_feedback']
if 'partially_correct_feedback' in config['DEFAULT']:
self.__partially_correct_feedback = config['DEFAULT']['partially_correct_feedback']
if 'incorrect_feedback' in config['DEFAULT']:
self.__incorrect_feedback = config['DEFAULT']['incorrect_feedback']
if 'adapt_images' in config['DEFAULT']:
self.__adapt_images = config['DEFAULT']['adapt_images'].lower()
if 'width' in config['DEFAULT']:
self.__width = config['DEFAULT']['width']
if 'height' in config['DEFAULT']:
self.__height = config['DEFAULT']['height']
[docs]
def define_from_csv(self, file, sep = ','):
"""Define questions from a csv file.
Each question is in a row. Each concept in a column.
Parameters
----------
file : str
Path to csv file.
sep : str
Separator character, ',' or ';'.
Returns
-------
None
Examples
--------
>>> q = Questions()
>>> q.define_from_csv('tests/questions.csv')
"""
self.__path = "%s" % Path(file).parent
self.__questions = read_csv(file, sep = sep)
[docs]
def define_from_excel(self, file, sheet_index = 0):
"""Define questions from an Excel file.
Each question is in a row. Each concept in a column.
Parameters
----------
file : str
Path to Excel file.
sheet_index : int
Number of sheet to process.
Returns
-------
None
Examples
--------
>>> q = Questions()
>>> q.define_from_excel('tests/questions.xlsx')
"""
self.__path = "%s" % Path(file).parent
with catch_warnings():
simplefilter("ignore")
self.__questions = read_excel(file, sheet_index)
[docs]
def generate_xml(self, file = None):
"""Generate quiz in XML file.
Each question is in a row. Each concept in a column.
Parameters
----------
file : str
Path to csv file.
Returns
-------
None
Examples
--------
>>> q = Questions()
>>> q.define_from_csv('tests/questions.csv')
>>> q.generate_xml('questions.xml')
"""
questions = self.__format_questions()
content = f"""<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<question type="category">
<category> <text>$course$/top/{self.__category}</text> </category>
<info format="html"> <text></text> </info>
<idnumber></idnumber>
</question>
{questions}
</quiz>"""
if not isna(file):
with open(file, "w") as text_file:
text_file.write(content)
return(content)
def __format_questions(self):
"""Format the questions included in the class."""
res = ''
if not isinstance(self.__questions, DataFrame):
return res
columns = self.__questions.columns
columns = columns.difference(['type', 'question', 'image', 'image_alt', 'answer'])
for index, row in self.__questions.iterrows():
res = res + self.__generate_question(row, index, columns)
return res
def __get_rest_of_answers(self, row, columns):
"""Get the rest of the answers from a row of questions."""
res = []
for col in columns:
if not isna(row[col]):
res.append(row[col])
return res
def __generate_question_text(self, row):
"""Generate the statement of a question, including the image if defined."""
if isna(row['image']):
img = ''
fimg = ''
else:
file = Path(row['image']).name
filename, file_extension = os.path.splitext(file)
image_alt = row['image_alt']
image = Image.open(self.__path + '/' + row['image'])
if self.__adapt_images == 'true':
image.thumbnail((int(self.__width), int(self.__height)))
width, height = image.size
fd, path = tempfile.mkstemp(suffix=file_extension)
image.save(path)
with open(path, "rb") as image_file:
value = b64encode(image_file.read()).decode('ascii')
img = f'<p><img src="@@PLUGINFILE@@/{file}" alt="{image_alt}" width="{width}" height="{height}" class="img-fluid atto_image_button_text-bottom"></p>'
fimg = f'<file name="{file}" path="/" encoding="base64">{value}</file>'
res = f"""
<questiontext format="html">
<text><![CDATA[
<!-- {self.__copyright} -->
<!-- {self.__license} -->
<p>{row['question']}</p>{img}]]></text>
{fimg}
</questiontext>
<generalfeedback format="html"> <text></text> </generalfeedback>
"""
return res
def __generate_name(self, row, index, type):
"""Generate the name of a question."""
num = int(self.__first_question_number) + index
question = row['question'][:40]
for p in punctuation:
question = question.replace(p, "")
question = question.replace(" ", "_")
name = "q%03d_%s_%s" % (num, type, question)
name = name.lower()
res = f"""
<name> <text>{name}</text> </name>
"""
return res
def __generate_question(self, row, index, columns):
"""Determine the type of question and generate it."""
questiontext = self.__generate_question_text(row)
rest = self.__get_rest_of_answers(row, columns)
answer = _string_to_vector(row["answer"])
if _is_numeric_answer(answer):
type = 'numerical'
question_type = '<question type="numerical">'
question_body = _generate_numerical(answer, rest)
elif len(rest) > 0:
if len(answer) == 1:
if not _has_gaps(row["question"]):
if isna(row["type"]):
type = 'multichoice'
question_type = '<question type="multichoice">'
question_body = _generate_multichoice(answer, rest, self.__correct_feedback, self.__incorrect_feedback)
else:
if row["type"].lower() == 'h':
orientation = 'h'
else:
orientation = 'v'
type = 'ordering'
question_type = '<question type="ordering">'
question_body = _generate_ordering(answer, rest, self.__correct_feedback, self.__partially_correct_feedback, self.__incorrect_feedback, orientation)
else:
if isna(row["type"]):
type = 'ddwtos'
question_type = '<question type="ddwtos">'
question_body = _generate_ddwtos(answer, rest, self.__correct_feedback, self.__partially_correct_feedback, self.__incorrect_feedback)
else:
type = 'gapselect'
question_type = '<question type="gapselect">'
question_body = _generate_gapselect(answer, rest, self.__correct_feedback, self.__partially_correct_feedback, self.__incorrect_feedback)
else:
type = 'matching'
question_type = '<question type="matching">'
question_body = _generate_matching(answer, rest, self.__correct_feedback, self.__partially_correct_feedback, self.__incorrect_feedback)
else:
if len(answer) == 0:
type = 'essay'
question_type = '<question type="essay">'
question_body = _generate_essay()
else:
if answer[0].lower() in ['true', 'false']:
type = 'truefalse'
question_type = '<question type="truefalse">'
question_body = _generate_truefalse(answer)
else:
type = 'shortanswer'
question_type = '<question type="shortanswer">'
question_body = _generate_shortanswer(answer)
name = self.__generate_name(row, index, type)
res = """
""" + question_type + name + questiontext + question_body + """
</question>"""
return res