# Licensed under GPLv3 # Written by Sebastian Lohff (seba@someserver.de) # http://seba-geek.de/projects/seopardy/ import os import yaml class QuestionException(Exception): """ Exception to be thrown when there is something wrong with the question file. """ pass class Questions(object): """ Object holding all the questions """ QUESTION_TYPES = ["Text", "Image", "Music", "Code", "Video"] QUESTION_KEYS = ["Name", "Question", "Answer", "Type", "Double-Jeopardy", "Audio"] def __init__(self, qfile, appendPath=False, verbose=False): self.qfile = qfile self._questions = [] self._appendPath = appendPath self._verbose = verbose self._title = "Round n" self._read_questions(self.qfile) def get_title(self): return self._title 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] 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) def _get_yaml(self, filename): f = None # open file try: f = open(filename) except OSError as e: raise QuestionException("Could not read question file '%s': %s" % (filename, e)) # load yaml ysrc = None try: ysrc = yaml.safe_load(f.read()) except Exception as e: 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 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) def _read_sections(self, ysrc, filename, jeopardyOverride=None): basedir = os.path.dirname(filename) # now to check the integrity of the question file if type(ysrc) is not list: self._gen_error(filename, "The questionfile has to be a list of sections") for i, sec in enumerate(ysrc, 1): if not "Section" in sec.keys() or not "Questions" in sec.keys(): self._gen_error(filename, "Section %d needs to have the keys 'Section' and 'Question' (case-sensitive)" % i) 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"]]): self._gen_error(filename, "Question %d from section %d (%s) is missing one of the keywords Question, Answer or Type" % (j, i, sec["Section"])) # 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"]))) # check for keys we do not know for key in q.keys(): if key not in self.QUESTION_KEYS: self._gen_error(filename, "Qestion %d from section %d (%s) has invalid keyword '%s'" % (j, i, sec["Section"], key)) # check Double-Jeopardy is a bool and is set to false if non-existant if "Double-Jeopardy" not in q.keys(): q["Double-Jeopardy"] = False elif type(q["Double-Jeopardy"]) != bool: 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"])) # 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: self._gen_error(filename, "The Audio key from question %d from section %d (%s) must be either true or false" % (j, i, sec["Section"])) # check for broken question types if q["Type"] not in self.QUESTION_TYPES: 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))) # check if file for music/image questions exist if q["Type"] in ("Music", "Image", "Video"): if self._appendPath: q["Question"] = os.path.join(basedir, q["Question"]) if not os.path.isfile(q["Question"]): 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)) # check if this section has enough questions if j != 5: 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) # check for only having unique section names sections = [s["Section"] for s in self._questions] if len(sections) != len(set(sections)): map(lambda _x: sections.remove(_x), set(sections)) self._gen_error(filename, "All section names must be unique (clashing: %s)" % (", ".join(sections),)) 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))