No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

question.py 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. # Licensed under GPLv3
  2. # Written by Sebastian Lohff (seba@someserver.de)
  3. # http://seba-geek.de/projects/seopardy/
  4. import os
  5. import yaml
  6. class QuestionException(Exception):
  7. """ Exception to be thrown when there is something wrong with
  8. the question file. """
  9. pass
  10. class Questions(object):
  11. """ Object holding all the questions """
  12. QUESTION_TYPES = ["Text", "Image", "Music", "Code", "Video"]
  13. QUESTION_KEYS = ["Name", "Question", "Answer", "Type", "Double-Jeopardy", "Audio"]
  14. def __init__(self, qfile, appendPath=False, verbose=False):
  15. self.qfile = qfile
  16. self._questions = []
  17. self._appendPath = appendPath
  18. self._verbose = verbose
  19. self._title = "Round n"
  20. self._read_questions(self.qfile)
  21. def get_title(self):
  22. return self._title
  23. def get_sections(self):
  24. return [s["Section"] for s in self._questions]
  25. def get_questions(self, section):
  26. sec = filter(lambda s: s["Section"] == section, self._questions)
  27. if len(sec) < 1:
  28. raise ValueError("Section %s does not exist" % (section,))
  29. return sec[0]["Questions"]
  30. def get_question(self, section, question):
  31. if type(question) != int or question < 1 or question > 5:
  32. raise ValueError("question parameter needs to be an integer between 1 and 5")
  33. return self.get_questions(section)[question-1]
  34. def get_number_from_section(self, section):
  35. for i,s in enumerate(self._questions, 1):
  36. if s["Section"] == section:
  37. return i
  38. raise ValueError("Section '%s' does not exist" % section)
  39. def _get_yaml(self, filename):
  40. f = None
  41. # open file
  42. try:
  43. f = open(filename)
  44. except OSError as e:
  45. raise QuestionException("Could not read question file '%s': %s" % (filename, e))
  46. # load yaml
  47. ysrc = None
  48. try:
  49. ysrc = yaml.safe_load(f.read())
  50. except Exception as e:
  51. raise QuestionException("Error parsing YAML in question file '%s': %s" % (filename, e))
  52. return ysrc
  53. def _gen_error(self, filename, error):
  54. raise QuestionException("Error parsing question file '%s': %s" % (filename, error))
  55. def _read_questions(self, filename):
  56. ysrc = self._get_yaml(filename)
  57. if type(ysrc) == list:
  58. # "old style" question file, list of sections
  59. # just read sections and return happily ever after
  60. self._read_sections(ysrc, filename)
  61. return True
  62. elif type(ysrc) == dict:
  63. # "new style" quetion file, dict of Name and Sections
  64. self._read_newstyle_questions(ysrc, filename)
  65. return True
  66. else:
  67. self._gen_error(filename, "Contents of YAML is neither a dict nor a list (format error), see seopardy guide for question file format documentation.")
  68. def _read_newstyle_questions(self, ysrc, filename):
  69. if type(ysrc) != dict:
  70. self._gen_error(filename, "Error parsing question file '%s': Is not a dict (and this sould(tm) have been safeguarded by an earlier if)")
  71. if "Name" not in ysrc.keys():
  72. self._gen_error(filename, "Missing 'Name' key.")
  73. if "Sections" not in ysrc.keys():
  74. self._gen_error(filename, "Missing 'Sections' key.")
  75. extra_keys = filter(lambda _x: _x not in ["Name", "Sections"], ysrc.keys())
  76. if extra_keys:
  77. self._gen_error(filename, "Unsupported keys found: %s (only 'Name' and 'Sections' are supported. Is your case correct?)" % (", ".join(extra_keys),))
  78. if type(ysrc["Sections"]) != list:
  79. self._gen_error(filename, "The 'Sections' part needs to be a list of filenames, not a '%s'" % (type(ysrc["Sections"]),))
  80. self._title = ysrc["Name"]
  81. # read sections
  82. basedir = os.path.dirname(filename)
  83. for n, sec_data in enumerate(ysrc["Sections"], 1):
  84. jeopardyOverride = None
  85. if type(sec_data) not in (unicode, str, dict):
  86. self._gen_error(filename, "Section element %d is neither a string nor a dict (type %s found)" % (n, type(sec_data)))
  87. sec_filename = None
  88. if type(sec_data) == dict:
  89. if "File" not in sec_data:
  90. self._gen_error(filename, "Section element %d is a dictionary, but has no key 'File' pointing to a file to load" % (n,))
  91. sec_filename = sec_data["File"]
  92. if "Double-Jeopardies" in sec_data and sec_data["Double-Jeopardies"] != None:
  93. # TODO: load (override) double jeopardies for section
  94. if type(sec_data["Double-Jeopardies"]) != list:
  95. self._gen_error(filename, "Section %d: Double-Jeopardies has to be a list or null (type %s found)" % (n, type(sec_data["Double-Jeopardies"])))
  96. # take care that jeopardyOverride is a list of 5 bools
  97. jeopardyOverride = map(bool, sec_data["Double-Jeopardies"])
  98. jeopardyOverride = jeopardyOverride[0:5]
  99. while len(jeopardyOverride) < 5:
  100. jeopardyOverride.append(False)
  101. else:
  102. sec_filename = sec_data
  103. fpath = os.path.join(basedir, sec_filename)
  104. sec_ysrc = None
  105. try:
  106. sec_ysrc = self._get_yaml(fpath)
  107. except (OSError, IOError) as e:
  108. raise QuestionException("Error reading question file %s: %s" % (fpath, str(e)))
  109. self._read_sections(sec_ysrc, fpath, jeopardyOverride)
  110. def _read_sections(self, ysrc, filename, jeopardyOverride=None):
  111. basedir = os.path.dirname(filename)
  112. # now to check the integrity of the question file
  113. if type(ysrc) is not list:
  114. self._gen_error(filename, "The questionfile has to be a list of sections")
  115. for i, sec in enumerate(ysrc, 1):
  116. if not "Section" in sec.keys() or not "Questions" in sec.keys():
  117. self._gen_error(filename, "Section %d needs to have the keys 'Section' and 'Question' (case-sensitive)" % i)
  118. for j, q in enumerate(sec["Questions"], 1):
  119. # check for keys we need in each question
  120. if any([x not in q.keys() for x in ["Question", "Answer", "Type"]]):
  121. self._gen_error(filename, "Question %d from section %d (%s) is missing one of the keywords Question, Answer or Type" % (j, i, sec["Section"]))
  122. # check wether the question is a string (else we'll get display errors)
  123. if type(q["Question"]) not in (str, unicode):
  124. 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"])))
  125. # check for keys we do not know
  126. for key in q.keys():
  127. if key not in self.QUESTION_KEYS:
  128. self._gen_error(filename, "Qestion %d from section %d (%s) has invalid keyword '%s'" % (j, i, sec["Section"], key))
  129. # check Double-Jeopardy is a bool and is set to false if non-existant
  130. if "Double-Jeopardy" not in q.keys():
  131. q["Double-Jeopardy"] = False
  132. elif type(q["Double-Jeopardy"]) != bool:
  133. 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"]))
  134. # handle Double-Jeopardy override by round file
  135. if jeopardyOverride:
  136. q["Double-Jeopardy"] = jeopardyOverride[j-1]
  137. # check Audio is a bool and is set to false if non-existant
  138. if "Audio" not in q.keys():
  139. q["Audio"] = False
  140. elif type(q["Audio"]) != bool:
  141. self._gen_error(filename, "The Audio key from question %d from section %d (%s) must be either true or false" % (j, i, sec["Section"]))
  142. # check for broken question types
  143. if q["Type"] not in self.QUESTION_TYPES:
  144. 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)))
  145. # check if file for music/image questions exist
  146. if q["Type"] in ("Music", "Image", "Video"):
  147. if self._appendPath:
  148. q["Question"] = os.path.join(basedir, q["Question"])
  149. if not os.path.isfile(q["Question"]):
  150. fname_error = ""
  151. if self._verbose:
  152. fname_error = " (path: %s)" % (q["Question"],)
  153. self._gen_error(filename, "File for question %d, section %d (%s) not found%s" % (j, i, sec["Section"], fname_error))
  154. # check if this section has enough questions
  155. if j != 5:
  156. self._gen_error(filename, "Section %d (%s) needs to have exactly %d questions (has %d)" % (i, sec["Section"], 5, j))
  157. # done, save yaml src to _questions
  158. self._questions.extend(ysrc)
  159. # check for only having unique section names
  160. sections = [s["Section"] for s in self._questions]
  161. if len(sections) != len(set(sections)):
  162. map(lambda _x: sections.remove(_x), set(sections))
  163. self._gen_error(filename, "All section names must be unique (clashing: %s)" % (", ".join(sections),))
  164. return True
  165. if __name__ == '__main__':
  166. q = Questions("questions/template.q")
  167. print(q.get_sections())
  168. print(q.get_questions("A"))
  169. print(q.get_question("A", 1))