Commit a51bcefdf1c7771cdaec4595f2c398a443d25fbb
1 parent
fa091c84
Exists in
master
and in
1 other branch
Many changes:
- drop support for python 3.6 - min_level in course configuration so that a topic is unlocked if dependencies are above the minimum level. - changed shuffle to shuffle_questions in course configuration to avoid confusion between the shuffle options of some questions. - correction no longer returns grade, just updates q['grade']. - remove prefix 'l' from students login name. - allow codemirror text boxes to grow. - updated yaml.load to yaml.safe_load (untested in regex).
Showing
11 changed files
with
46 additions
and
58 deletions
Show diff stats
BUGS.md
| 1 | 1 | |
| 2 | 2 | # BUGS |
| 3 | 3 | |
| 4 | +- testar se perguntas regex funcionam com yaml.safe_load. | |
| 4 | 5 | - safari as vezes envia dois gets no inicio do topico. nesses casos, a segunda pergunta não é actualizada no browser... o topico tem de ser gerado qd se escolhe o topico em main_topics. O get nao deve alterar o estado. |
| 5 | 6 | - click numa opcao checkbox fora da checkbox+label não está a funcionar. |
| 6 | 7 | - shift-enter não está a funcionar | ... | ... |
aprendizations/factory.py
| ... | ... | @@ -48,17 +48,17 @@ class QFactory(object): |
| 48 | 48 | # Depending on the type of question, a different question class will be |
| 49 | 49 | # instantiated. All these classes derive from the base class `Question`. |
| 50 | 50 | _types = { |
| 51 | - 'radio': QuestionRadio, | |
| 52 | - 'checkbox': QuestionCheckbox, | |
| 53 | - 'text': QuestionText, | |
| 54 | - 'text-regex': QuestionTextRegex, | |
| 51 | + 'radio': QuestionRadio, | |
| 52 | + 'checkbox': QuestionCheckbox, | |
| 53 | + 'text': QuestionText, | |
| 54 | + 'text-regex': QuestionTextRegex, | |
| 55 | 55 | 'numeric-interval': QuestionNumericInterval, |
| 56 | - 'textarea': QuestionTextArea, | |
| 56 | + 'textarea': QuestionTextArea, | |
| 57 | 57 | # -- informative panels -- |
| 58 | - 'information': QuestionInformation, | |
| 59 | - 'warning': QuestionInformation, | |
| 60 | - 'alert': QuestionInformation, | |
| 61 | - 'success': QuestionInformation, | |
| 58 | + 'information': QuestionInformation, | |
| 59 | + 'warning': QuestionInformation, | |
| 60 | + 'alert': QuestionInformation, | |
| 61 | + 'success': QuestionInformation, | |
| 62 | 62 | } |
| 63 | 63 | |
| 64 | 64 | def __init__(self, question_dict={}): |
| ... | ... | @@ -69,7 +69,7 @@ class QFactory(object): |
| 69 | 69 | # i.e. a question object (radio, checkbox, ...). |
| 70 | 70 | # ----------------------------------------------------------------------- |
| 71 | 71 | # async def generate_async(self): |
| 72 | - # loop = asyncio.get_event_loop() | |
| 72 | + # loop = asyncio.get_running_loop() | |
| 73 | 73 | # return await loop.run_in_executor(None, self.generate) |
| 74 | 74 | |
| 75 | 75 | def generate(self): | ... | ... |
aprendizations/knowledge.py
| ... | ... | @@ -43,34 +43,33 @@ class StudentKnowledge(object): |
| 43 | 43 | now = datetime.now() |
| 44 | 44 | for tref, s in self.state.items(): |
| 45 | 45 | dt = now - s['date'] |
| 46 | - s['level'] *= 0.95 ** dt.days # forgetting factor 0.95 FIXME | |
| 46 | + s['level'] *= 0.98 ** dt.days # forgetting factor 0.95 FIXME | |
| 47 | 47 | |
| 48 | 48 | # ------------------------------------------------------------------------ |
| 49 | 49 | # Unlock topics whose dependencies are satisfied (> min_level) |
| 50 | 50 | # ------------------------------------------------------------------------ |
| 51 | 51 | def unlock_topics(self): |
| 52 | - # minimum level that the dependencies of a topic must have | |
| 53 | - # for the topic to be unlocked. | |
| 54 | - min_level = 0.01 | |
| 55 | - | |
| 56 | 52 | for topic in self.deps.nodes(): |
| 57 | 53 | if topic not in self.state: # if locked |
| 58 | 54 | pred = self.deps.predecessors(topic) |
| 55 | + min_level = self.deps.node[topic]['min_level'] | |
| 59 | 56 | if all(d in self.state and self.state[d]['level'] > min_level |
| 60 | - for d in pred): # all dependencies are done | |
| 57 | + for d in pred): # all deps are greater than min_level | |
| 61 | 58 | |
| 62 | 59 | self.state[topic] = { |
| 63 | 60 | 'level': 0.0, # unlocked |
| 64 | 61 | 'date': datetime.now() |
| 65 | 62 | } |
| 66 | 63 | logger.debug(f'Unlocked "{topic}".') |
| 64 | + # else: # lock this topic if deps do not satisfy min_level | |
| 65 | + # del self.state[topic] | |
| 67 | 66 | |
| 68 | 67 | # ------------------------------------------------------------------------ |
| 69 | 68 | # Start a new topic. |
| 70 | 69 | # questions: list of generated questions to do in the topic |
| 71 | 70 | # current_question: the current question to be presented |
| 72 | 71 | # ------------------------------------------------------------------------ |
| 73 | - # FIXME async mas nao tem awaits... | |
| 72 | + # FIXME async mas nao tem awaits... do not allow restart same topic | |
| 74 | 73 | async def start_topic(self, topic): |
| 75 | 74 | logger.debug(f'StudentKnowledge.start_topic({topic})') |
| 76 | 75 | if self.current_topic == topic: |
| ... | ... | @@ -88,7 +87,7 @@ class StudentKnowledge(object): |
| 88 | 87 | |
| 89 | 88 | t = self.deps.node[topic] |
| 90 | 89 | k = t['choose'] |
| 91 | - if t['shuffle']: | |
| 90 | + if t['shuffle_questions']: | |
| 92 | 91 | questions = random.sample(t['questions'], k=k) |
| 93 | 92 | else: |
| 94 | 93 | questions = t['questions'][:k] |
| ... | ... | @@ -131,11 +130,10 @@ class StudentKnowledge(object): |
| 131 | 130 | q = self.current_question |
| 132 | 131 | q['answer'] = answer |
| 133 | 132 | q['finish_time'] = datetime.now() |
| 134 | - grade = await q.correct_async() | |
| 135 | - | |
| 136 | - logger.debug(f'Grade {grade:.2} ({q["ref"]})') | |
| 133 | + await q.correct_async() | |
| 134 | + logger.debug(f'Grade {q["grade"]:.2} ({q["ref"]})') | |
| 137 | 135 | |
| 138 | - if grade > 0.999: | |
| 136 | + if q['grade'] > 0.999: | |
| 139 | 137 | self.correct_answers += 1 |
| 140 | 138 | self.next_question() |
| 141 | 139 | action = 'right' | ... | ... |
aprendizations/learnapp.py
| ... | ... | @@ -26,8 +26,7 @@ logger = logging.getLogger(__name__) |
| 26 | 26 | # helper functions |
| 27 | 27 | # ============================================================================ |
| 28 | 28 | async def _bcrypt_hash(a, b): |
| 29 | - # loop = asyncio.get_running_loop() # FIXME python 3.7 only | |
| 30 | - loop = asyncio.get_event_loop() | |
| 29 | + loop = asyncio.get_running_loop() | |
| 31 | 30 | return await loop.run_in_executor(None, bcrypt.hashpw, |
| 32 | 31 | a.encode('utf-8'), b) |
| 33 | 32 | |
| ... | ... | @@ -248,11 +247,12 @@ class LearnApp(object): |
| 248 | 247 | |
| 249 | 248 | # default attributes that apply to the topics |
| 250 | 249 | default_file = config.get('file', 'questions.yaml') |
| 251 | - default_shuffle = config.get('shuffle', True) | |
| 250 | + default_shuffle_questions = config.get('shuffle_questions', True) | |
| 252 | 251 | default_choose = config.get('choose', 9999) |
| 253 | 252 | default_forgetting_factor = config.get('forgetting_factor', 1.0) |
| 254 | 253 | default_maxtries = config.get('max_tries', 3) |
| 255 | 254 | default_append_wrong = config.get('append_wrong', True) |
| 255 | + default_min_level = config.get('min_level', 0.01) # to unlock topic | |
| 256 | 256 | |
| 257 | 257 | # iterate over topics and populate graph |
| 258 | 258 | topics = config.get('topics', {}) |
| ... | ... | @@ -267,10 +267,11 @@ class LearnApp(object): |
| 267 | 267 | t['name'] = attr.get('name', tref) |
| 268 | 268 | t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic |
| 269 | 269 | t['file'] = attr.get('file', default_file) # questions.yaml |
| 270 | - t['shuffle'] = attr.get('shuffle', default_shuffle) | |
| 270 | + t['shuffle_questions'] = attr.get('shuffle_questions', default_shuffle_questions) | |
| 271 | 271 | t['max_tries'] = attr.get('max_tries', default_maxtries) |
| 272 | 272 | t['forgetting_factor'] = attr.get('forgetting_factor', |
| 273 | 273 | default_forgetting_factor) |
| 274 | + t['min_level'] = attr.get('min_level', default_min_level) | |
| 274 | 275 | t['choose'] = attr.get('choose', default_choose) |
| 275 | 276 | t['append_wrong'] = attr.get('append_wrong', default_append_wrong) |
| 276 | 277 | t['questions'] = attr.get('questions', []) | ... | ... |
aprendizations/questions.py
| ... | ... | @@ -43,17 +43,15 @@ class Question(dict): |
| 43 | 43 | 'files': {}, |
| 44 | 44 | }) |
| 45 | 45 | |
| 46 | - def correct(self) -> float: | |
| 46 | + def correct(self) -> None: | |
| 47 | 47 | self['comments'] = '' |
| 48 | 48 | self['grade'] = 0.0 |
| 49 | - return 0.0 | |
| 50 | 49 | |
| 51 | - async def correct_async(self) -> float: | |
| 52 | - loop = asyncio.get_running_loop() # FIXME python 3.7 only | |
| 53 | - # loop = asyncio.get_event_loop() # python 3.6 | |
| 54 | - return await loop.run_in_executor(None, self.correct) | |
| 50 | + async def correct_async(self) -> None: | |
| 51 | + loop = asyncio.get_running_loop() | |
| 52 | + await loop.run_in_executor(None, self.correct) | |
| 55 | 53 | |
| 56 | - def set_defaults(self, d): | |
| 54 | + def set_defaults(self, d) -> None: | |
| 57 | 55 | 'Add k:v pairs from default dict d for nonexistent keys' |
| 58 | 56 | for k, v in d.items(): |
| 59 | 57 | self.setdefault(k, v) |
| ... | ... | @@ -204,8 +202,6 @@ class QuestionCheckbox(Question): |
| 204 | 202 | |
| 205 | 203 | self['grade'] = x / sum_abs |
| 206 | 204 | |
| 207 | - return self['grade'] | |
| 208 | - | |
| 209 | 205 | |
| 210 | 206 | # =========================================================================== |
| 211 | 207 | class QuestionText(Question): |
| ... | ... | @@ -239,8 +235,6 @@ class QuestionText(Question): |
| 239 | 235 | if self['answer'] is not None: |
| 240 | 236 | self['grade'] = 1.0 if self['answer'] in self['correct'] else 0.0 |
| 241 | 237 | |
| 242 | - return self['grade'] | |
| 243 | - | |
| 244 | 238 | |
| 245 | 239 | # =========================================================================== |
| 246 | 240 | class QuestionTextRegex(Question): |
| ... | ... | @@ -271,8 +265,6 @@ class QuestionTextRegex(Question): |
| 271 | 265 | f'answer {self["answer"]}.') |
| 272 | 266 | self['grade'] = 1.0 if ok else 0.0 |
| 273 | 267 | |
| 274 | - return self['grade'] | |
| 275 | - | |
| 276 | 268 | |
| 277 | 269 | # =========================================================================== |
| 278 | 270 | class QuestionNumericInterval(Question): |
| ... | ... | @@ -308,8 +300,6 @@ class QuestionNumericInterval(Question): |
| 308 | 300 | else: |
| 309 | 301 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 |
| 310 | 302 | |
| 311 | - return self['grade'] | |
| 312 | - | |
| 313 | 303 | |
| 314 | 304 | # =========================================================================== |
| 315 | 305 | class QuestionTextArea(Question): |
| ... | ... | @@ -358,8 +348,6 @@ class QuestionTextArea(Question): |
| 358 | 348 | else: |
| 359 | 349 | self['grade'] = float(out) |
| 360 | 350 | |
| 361 | - return self['grade'] | |
| 362 | - | |
| 363 | 351 | |
| 364 | 352 | # =========================================================================== |
| 365 | 353 | class QuestionInformation(Question): |
| ... | ... | @@ -375,4 +363,3 @@ class QuestionInformation(Question): |
| 375 | 363 | def correct(self): |
| 376 | 364 | super().correct() |
| 377 | 365 | self['grade'] = 1.0 # always "correct" but points should be zero! |
| 378 | - return self['grade'] | ... | ... |
aprendizations/serve.py
| ... | ... | @@ -95,7 +95,7 @@ class LoginHandler(BaseHandler): |
| 95 | 95 | self.render('login.html', error='') |
| 96 | 96 | |
| 97 | 97 | async def post(self): |
| 98 | - uid = self.get_body_argument('uid') | |
| 98 | + uid = self.get_body_argument('uid').lstrip('l') | |
| 99 | 99 | pw = self.get_body_argument('pw') |
| 100 | 100 | |
| 101 | 101 | login_ok = await self.learn.login(uid, pw) | ... | ... |
aprendizations/static/css/topic.css
aprendizations/templates/question-textarea.html
| ... | ... | @@ -7,10 +7,9 @@ |
| 7 | 7 | |
| 8 | 8 | <script> |
| 9 | 9 | var editor = CodeMirror.fromTextArea(document.getElementById("code"), { |
| 10 | - lineNumbers: true, | |
| 11 | - mode: "clike", | |
| 12 | - // matchBrackets: true, | |
| 13 | - // theme: "night", | |
| 10 | + lineNumbers: true, | |
| 11 | + viewportMargin: Infinity, | |
| 12 | + // styleActiveLine: true, | |
| 14 | 13 | }); |
| 15 | 14 | </script> |
| 16 | 15 | ... | ... |
aprendizations/templates/topic.html
| ... | ... | @@ -32,8 +32,8 @@ |
| 32 | 32 | <script defer src="/static/mdbootstrap/js/bootstrap.min.js"></script> |
| 33 | 33 | <script defer src="/static/mdbootstrap/js/mdb.min.js"></script> |
| 34 | 34 | <script defer src="/static/fontawesome-free/js/all.min.js"></script> |
| 35 | - <script defer src="/static/codemirror/lib/codemirror.js"></script> | |
| 36 | - <script defer src="/static/js/topic.js"></script> | |
| 35 | + <script defer src="/static/codemirror/lib/codemirror.js"></script> | |
| 36 | + <script defer src="/static/js/topic.js"></script> | |
| 37 | 37 | |
| 38 | 38 | </head> |
| 39 | 39 | <!-- ===================================================================== --> | ... | ... |
aprendizations/tools.py
| ... | ... | @@ -147,7 +147,7 @@ def load_yaml(filename, default=None): |
| 147 | 147 | else: |
| 148 | 148 | with f: |
| 149 | 149 | try: |
| 150 | - default = yaml.load(f) | |
| 150 | + default = yaml.safe_load(f) # FIXME check if supports all kinds of questions including regex | |
| 151 | 151 | except yaml.YAMLError as e: |
| 152 | 152 | mark = e.problem_mark |
| 153 | 153 | logger.error(f'In file "{filename}" near line {mark.line}, ' |
| ... | ... | @@ -187,7 +187,7 @@ def run_script(script, args=[], stdin='', timeout=5): |
| 187 | 187 | logger.error(f'Return code {p.returncode} running "{script}".') |
| 188 | 188 | else: |
| 189 | 189 | try: |
| 190 | - output = yaml.load(p.stdout) | |
| 190 | + output = yaml.safe_load(p.stdout) | |
| 191 | 191 | except Exception: |
| 192 | 192 | logger.error(f'Error parsing yaml output of "{script}"') |
| 193 | 193 | else: | ... | ... |
demo/demo.yaml
| ... | ... | @@ -5,12 +5,13 @@ database: students.db |
| 5 | 5 | |
| 6 | 6 | |
| 7 | 7 | # values applie to each topic, if undefined there |
| 8 | -# default values are: file=question.yaml, shuffle=True, choose: all | |
| 9 | 8 | file: questions.yaml |
| 10 | -shuffle: false | |
| 9 | +shuffle_questions: true | |
| 11 | 10 | choose: 6 |
| 12 | 11 | max_tries: 2 |
| 13 | -forgetting_factor: 0.99 | |
| 12 | +forgetting_factor: 0.97 | |
| 13 | +min_level: 0.01 | |
| 14 | +append_wrong: true | |
| 14 | 15 | |
| 15 | 16 | # ---------------------------------------------------------------------------- |
| 16 | 17 | topics: | ... | ... |