seopardy/question.py

220 lines
8.2 KiB
Python
Raw Normal View History

2015-08-19 11:20:51 +02:00
# Licensed under GPLv3
# Written by Sebastian Lohff (seba@someserver.de)
# http://seba-geek.de/projects/seopardy/
2013-11-05 17:16:21 +01:00
import os
import yaml
2013-11-06 20:42:00 +01:00
2013-11-05 17:16:21 +01:00
class QuestionException(Exception):
2013-11-06 20:42:00 +01:00
""" Exception to be thrown when there is something wrong with
the question file. """
2013-11-05 17:16:21 +01:00
pass
class Questions(object):
2013-11-06 20:42:00 +01:00
""" Object holding all the questions """
QUESTION_TYPES = ["Text", "Image", "Music", "Code", "Video"]
QUESTION_KEYS = ["Name", "Question", "Answer", "Type", "Double-Jeopardy", "Audio"]
2013-11-06 20:42:00 +01:00
2014-12-27 13:11:21 +01:00
def __init__(self, qfile, appendPath=False, verbose=False):
2013-11-05 17:16:21 +01:00
self.qfile = qfile
2014-12-27 13:11:21 +01:00
self._questions = []
self._appendPath = appendPath
2014-12-27 13:11:21 +01:00
self._verbose = verbose
self._title = "Round n"
self._read_questions(self.qfile)
def get_title(self):
return self._title
2013-11-05 17:16:21 +01:00
def get_sections(self):
return [s["Section"] for s in self._questions]
def get_questions(self, section):
sec = filter(lambda s: s["Section"] == section, self._questions)
if len(sec) < 1:
raise ValueError("Section %s does not exist" % (section,))
return sec[0]["Questions"]
def get_question(self, section, question):
if type(question) != int or question < 1 or question > 5:
raise ValueError("question parameter needs to be an integer between 1 and 5")
return self.get_questions(section)[question-1]
2013-11-11 00:52:37 +01:00
def get_number_from_section(self, section):
for i,s in enumerate(self._questions, 1):
if s["Section"] == section:
return i
raise ValueError("Section '%s' does not exist" % section)
2014-12-27 13:11:21 +01:00
def _get_yaml(self, filename):
2013-11-05 17:16:21 +01:00
f = None
# open file
try:
2014-12-27 13:11:21 +01:00
f = open(filename)
2013-11-05 17:16:21 +01:00
except OSError as e:
2014-12-27 13:11:21 +01:00
raise QuestionException("Could not read question file '%s': %s" % (filename, e))
2013-11-05 17:16:21 +01:00
# load yaml
ysrc = None
try:
ysrc = yaml.safe_load(f.read())
2013-11-05 17:16:21 +01:00
except Exception as e:
2014-12-27 13:11:21 +01:00
raise QuestionException("Error parsing YAML in question file '%s': %s" % (filename, e))
return ysrc
def _gen_error(self, filename, error):
raise QuestionException("Error parsing question file '%s': %s" % (filename, error))
def _read_questions(self, filename):
ysrc = self._get_yaml(filename)
if type(ysrc) == list:
# "old style" question file, list of sections
# just read sections and return happily ever after
self._read_sections(ysrc, filename)
return True
elif type(ysrc) == dict:
# "new style" quetion file, dict of Name and Sections
self._read_newstyle_questions(ysrc, filename)
return True
else:
self._gen_error(filename, "Contents of YAML is neither a dict nor a list (format error), see seopardy guide for question file format documentation.")
def _read_newstyle_questions(self, ysrc, filename):
if type(ysrc) != dict:
self._gen_error(filename, "Error parsing question file '%s': Is not a dict (and this sould(tm) have been safeguarded by an earlier if)")
if "Name" not in ysrc.keys():
self._gen_error(filename, "Missing 'Name' key.")
if "Sections" not in ysrc.keys():
self._gen_error(filename, "Missing 'Sections' key.")
extra_keys = filter(lambda _x: _x not in ["Name", "Sections"], ysrc.keys())
if extra_keys:
self._gen_error(filename, "Unsupported keys found: %s (only 'Name' and 'Sections' are supported. Is your case correct?)" % (", ".join(extra_keys),))
if type(ysrc["Sections"]) != list:
self._gen_error(filename, "The 'Sections' part needs to be a list of filenames, not a '%s'" % (type(ysrc["Sections"]),))
self._title = ysrc["Name"]
# read sections
basedir = os.path.dirname(filename)
for n, sec_data in enumerate(ysrc["Sections"], 1):
jeopardyOverride = None
if type(sec_data) not in (unicode, str, dict):
self._gen_error(filename, "Section element %d is neither a string nor a dict (type %s found)" % (n, type(sec_data)))
sec_filename = None
if type(sec_data) == dict:
if "File" not in sec_data:
self._gen_error(filename, "Section element %d is a dictionary, but has no key 'File' pointing to a file to load" % (n,))
sec_filename = sec_data["File"]
if "Double-Jeopardies" in sec_data and sec_data["Double-Jeopardies"] != None:
# TODO: load (override) double jeopardies for section
if type(sec_data["Double-Jeopardies"]) != list:
self._gen_error(filename, "Section %d: Double-Jeopardies has to be a list or null (type %s found)" % (n, type(sec_data["Double-Jeopardies"])))
# take care that jeopardyOverride is a list of 5 bools
jeopardyOverride = map(bool, sec_data["Double-Jeopardies"])
jeopardyOverride = jeopardyOverride[0:5]
while len(jeopardyOverride) < 5:
jeopardyOverride.append(False)
else:
sec_filename = sec_data
2014-12-27 13:11:21 +01:00
fpath = os.path.join(basedir, sec_filename)
sec_ysrc = None
try:
sec_ysrc = self._get_yaml(fpath)
except (OSError, IOError) as e:
raise QuestionException("Error reading question file %s: %s" % (fpath, str(e)))
self._read_sections(sec_ysrc, fpath, jeopardyOverride)
2014-12-27 13:11:21 +01:00
def _read_sections(self, ysrc, filename, jeopardyOverride=None):
2014-12-27 13:11:21 +01:00
basedir = os.path.dirname(filename)
2013-11-05 17:16:21 +01:00
# now to check the integrity of the question file
if type(ysrc) is not list:
2014-12-27 13:11:21 +01:00
self._gen_error(filename, "The questionfile has to be a list of sections")
2013-11-05 17:16:21 +01:00
for i, sec in enumerate(ysrc, 1):
if not "Section" in sec.keys() or not "Questions" in sec.keys():
2014-12-27 13:11:21 +01:00
self._gen_error(filename, "Section %d needs to have the keys 'Section' and 'Question' (case-sensitive)" % i)
2013-11-05 17:16:21 +01:00
for j, q in enumerate(sec["Questions"], 1):
# check for keys we need in each question
if any([x not in q.keys() for x in ["Question", "Answer", "Type"]]):
2014-12-27 13:11:21 +01:00
self._gen_error(filename, "Question %d from section %d (%s) is missing one of the keywords Question, Answer or Type" % (j, i, sec["Section"]))
2013-11-05 17:16:21 +01:00
# check wether the question is a string (else we'll get display errors)
if type(q["Question"]) not in (str, unicode):
self._gen_error(filename, "Question %d from section %d (%s) needs to have a string as question (type found was '%s', maybe put the Question in \"\")" % (j, i, sec["Section"], type(q["Question"])))
2013-11-05 17:16:21 +01:00
# check for keys we do not know
for key in q.keys():
if key not in self.QUESTION_KEYS:
2014-12-27 13:11:21 +01:00
self._gen_error(filename, "Qestion %d from section %d (%s) has invalid keyword '%s'" % (j, i, sec["Section"], key))
2013-11-05 17:16:21 +01:00
# check Double-Jeopardy is a bool and is set to false if non-existant
2013-11-05 17:16:21 +01:00
if "Double-Jeopardy" not in q.keys():
q["Double-Jeopardy"] = False
elif type(q["Double-Jeopardy"]) != bool:
2014-12-27 13:11:21 +01:00
self._gen_error(filename, "The Double-Jeopardy key from question %d from section %d (%s) must be either true or false" % (j, i, sec["Section"]))
2013-11-05 17:16:21 +01:00
# handle Double-Jeopardy override by round file
if jeopardyOverride:
q["Double-Jeopardy"] = jeopardyOverride[j-1]
# check Audio is a bool and is set to false if non-existant
if "Audio" not in q.keys():
q["Audio"] = False
elif type(q["Audio"]) != bool:
2014-12-27 13:11:21 +01:00
self._gen_error(filename, "The Audio key from question %d from section %d (%s) must be either true or false" % (j, i, sec["Section"]))
2013-11-05 17:16:21 +01:00
# check for broken question types
if q["Type"] not in self.QUESTION_TYPES:
2014-12-27 13:11:21 +01:00
self._gen_error(filename, "Question %d from Section %d (%s) has an invalid type '%s' (valid types are %s)" % (j, i, sec["Section"], q["Type"], ", ".join(self.QUESTION_TYPES)))
2013-11-05 17:16:21 +01:00
# check if file for music/image questions exist
if q["Type"] in ("Music", "Image", "Video"):
if self._appendPath:
2014-12-27 13:11:21 +01:00
q["Question"] = os.path.join(basedir, q["Question"])
2013-11-05 17:16:21 +01:00
if not os.path.isfile(q["Question"]):
2014-12-27 13:11:21 +01:00
fname_error = ""
if self._verbose:
fname_error = " (path: %s)" % (q["Question"],)
self._gen_error(filename, "File for question %d, section %d (%s) not found%s" % (j, i, sec["Section"], fname_error))
2013-11-05 17:16:21 +01:00
# check if this section has enough questions
if j != 5:
2014-12-27 13:11:21 +01:00
self._gen_error(filename, "Section %d (%s) needs to have exactly %d questions (has %d)" % (i, sec["Section"], 5, j))
# done, save yaml src to _questions
self._questions.extend(ysrc)
2013-11-05 17:16:21 +01:00
# check for only having unique section names
2014-12-27 13:11:21 +01:00
sections = [s["Section"] for s in self._questions]
2013-11-05 17:16:21 +01:00
if len(sections) != len(set(sections)):
2014-12-27 13:11:21 +01:00
map(lambda _x: sections.remove(_x), set(sections))
self._gen_error(filename, "All section names must be unique (clashing: %s)" % (", ".join(sections),))
2013-11-05 17:16:21 +01:00
return True
if __name__ == '__main__':
q = Questions("questions/template.q")
print(q.get_sections())
print(q.get_questions("A"))
print(q.get_question("A", 1))