Commit 9ff401330259b2f642614cbd94296f4e4aa3f32c

Authored by Miguel Barão
1 parent b7b63323
Exists in master and in 1 other branch dev

add checks to:

- duplicate ref in the same file
- options file in radio and checkbox questions
load_yaml can raise more useful exceptions
BUGS.md
... ... @@ -31,6 +31,7 @@
31 31  
32 32 # FIXED
33 33  
  34 +- mesma ref no mesmo ficheiro não é detectado.
34 35 - enter nas respostas mostra json
35 36 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande)
36 37 - double click submits twice.
... ...
aprendizations/learnapp.py
... ... @@ -90,7 +90,6 @@ class LearnApp(object):
90 90 for c, d in self.courses.items():
91 91 for goal in d['goals']:
92 92 if goal not in self.deps.nodes():
93   - # logger.error(f'Goal "{goal}" of "{c}"" not in the graph')
94 93 raise LearnException(f'Goal "{goal}" from course "{c}" '
95 94 ' does not exist')
96 95  
... ... @@ -409,14 +408,24 @@ class LearnApp(object):
409 408 fullpath: str = path.join(topicpath, t['file'])
410 409  
411 410 logger.debug(f' Loading {fullpath}')
412   - questions: List[QDict] = load_yaml(fullpath, default=[])
  411 + # questions: List[QDict] = load_yaml(fullpath, default=[])
  412 + try:
  413 + questions: List[QDict] = load_yaml(fullpath)
  414 + except Exception:
  415 + raise LearnException(f'Failed to load "{fullpath}"')
413 416  
414 417 # update refs to include topic as prefix.
415 418 # refs are required to be unique only within the file.
416 419 # undefined are set to topic:n, where n is the question number
417 420 # within the file
  421 + localrefs = set() # refs in current file
418 422 for i, q in enumerate(questions):
419 423 qref = q.get('ref', str(i)) # ref or number
  424 + if qref in localrefs:
  425 + msg = f'Duplicate ref "{qref}" in "{topicpath}"'
  426 + raise LearnException(msg)
  427 + localrefs.add(qref)
  428 +
420 429 q['ref'] = f'{tref}:{qref}'
421 430 q['path'] = topicpath
422 431 q.setdefault('append_wrong', t['append_wrong'])
... ...
aprendizations/questions.py
... ... @@ -74,7 +74,14 @@ class QuestionRadio(Question):
74 74 def __init__(self, q: QDict) -> None:
75 75 super().__init__(q)
76 76  
77   - n = len(self['options'])
  77 + try:
  78 + n = len(self['options'])
  79 + except KeyError:
  80 + msg = f'Missing `options` in radio question. See {self["path"]}'
  81 + raise QuestionException(msg)
  82 + except TypeError:
  83 + msg = f'`options` must be a list. See {self["path"]}'
  84 + raise QuestionException(msg)
78 85  
79 86 self.set_defaults(QDict({
80 87 'text': '',
... ... @@ -185,7 +192,14 @@ class QuestionCheckbox(Question):
185 192 def __init__(self, q: QDict) -> None:
186 193 super().__init__(q)
187 194  
188   - n = len(self['options'])
  195 + try:
  196 + n = len(self['options'])
  197 + except KeyError:
  198 + msg = f'Missing `options` in radio question. See {self["path"]}'
  199 + raise QuestionException(msg)
  200 + except TypeError:
  201 + msg = f'`options` must be a list. See {self["path"]}'
  202 + raise QuestionException(msg)
189 203  
190 204 # set defaults if missing
191 205 self.set_defaults(QDict({
... ...
aprendizations/tools.py
... ... @@ -111,14 +111,13 @@ class HighlightRenderer(mistune.Renderer):
111 111  
112 112 def table(self, header, body):
113 113 return ('<table class="table table-sm"><thead class="thead-light">'
114   - f'{header}</thead><tbody>{body}</tbody></table>')
  114 + f'{header}</thead><tbody>{body}</tbody></table>')
115 115  
116 116 def image(self, src, title, alt):
117 117 alt = mistune.escape(alt, quote=True)
118 118 title = mistune.escape(title or '', quote=True)
119 119 return (f'<img src="/file/{src}" alt="{alt}" title="{title}"'
120   - 'class="img-fluid">')
121   - # class="img-fluid mx-auto d-block"
  120 + f'class="img-fluid">') # class="img-fluid mx-auto d-block"
122 121  
123 122 # Pass math through unaltered - mathjax does the rendering in the browser
124 123 def block_math(self, text):
... ... @@ -150,25 +149,22 @@ def load_yaml(filename: str, default: Any = None) -&gt; Any:
150 149 filename = path.expanduser(filename)
151 150 try:
152 151 f = open(filename, 'r', encoding='utf-8')
153   - except FileNotFoundError:
154   - logger.error(f'Cannot open "{filename}": not found')
155   - except PermissionError:
156   - logger.error(f'Cannot open "{filename}": no permission')
157   - except OSError:
158   - logger.error(f'Cannot open file "{filename}"')
159   - else:
160   - with f:
161   - try:
162   - default = yaml.safe_load(f)
163   - except yaml.YAMLError as e:
164   - if hasattr(e, 'problem_mark'):
165   - mark = e.problem_mark
166   - logger.error(f'File "{filename}" near line {mark.line+1}, '
167   - f'column {mark.column+1}')
168   - else:
169   - logger.error(f'File "{filename}"')
170   - finally:
171   - return default
  152 + except Exception as e:
  153 + logger.error(e)
  154 + if default is not None:
  155 + return default
  156 + else:
  157 + raise
  158 +
  159 + with f:
  160 + try:
  161 + return yaml.safe_load(f)
  162 + except yaml.YAMLError as e:
  163 + logger.error(str(e).replace('\n', ' '))
  164 + if default is not None:
  165 + return default
  166 + else:
  167 + raise
172 168  
173 169  
174 170 # ---------------------------------------------------------------------------
... ...