From a51bcefdf1c7771cdaec4595f2c398a443d25fbb Mon Sep 17 00:00:00 2001 From: Miguel Barão Date: Thu, 14 Mar 2019 21:09:44 +0000 Subject: [PATCH] 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). --- BUGS.md | 1 + aprendizations/factory.py | 20 ++++++++++---------- aprendizations/knowledge.py | 22 ++++++++++------------ aprendizations/learnapp.py | 9 +++++---- aprendizations/questions.py | 23 +++++------------------ aprendizations/serve.py | 2 +- aprendizations/static/css/topic.css | 5 +++-- aprendizations/templates/question-textarea.html | 7 +++---- aprendizations/templates/topic.html | 4 ++-- aprendizations/tools.py | 4 ++-- demo/demo.yaml | 7 ++++--- 11 files changed, 46 insertions(+), 58 deletions(-) diff --git a/BUGS.md b/BUGS.md index 55a96a2..85d7255 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,6 +1,7 @@ # BUGS +- testar se perguntas regex funcionam com yaml.safe_load. - 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. - click numa opcao checkbox fora da checkbox+label não está a funcionar. - shift-enter não está a funcionar diff --git a/aprendizations/factory.py b/aprendizations/factory.py index ea9178a..cbc3739 100644 --- a/aprendizations/factory.py +++ b/aprendizations/factory.py @@ -48,17 +48,17 @@ class QFactory(object): # Depending on the type of question, a different question class will be # instantiated. All these classes derive from the base class `Question`. _types = { - 'radio': QuestionRadio, - 'checkbox': QuestionCheckbox, - 'text': QuestionText, - 'text-regex': QuestionTextRegex, + 'radio': QuestionRadio, + 'checkbox': QuestionCheckbox, + 'text': QuestionText, + 'text-regex': QuestionTextRegex, 'numeric-interval': QuestionNumericInterval, - 'textarea': QuestionTextArea, + 'textarea': QuestionTextArea, # -- informative panels -- - 'information': QuestionInformation, - 'warning': QuestionInformation, - 'alert': QuestionInformation, - 'success': QuestionInformation, + 'information': QuestionInformation, + 'warning': QuestionInformation, + 'alert': QuestionInformation, + 'success': QuestionInformation, } def __init__(self, question_dict={}): @@ -69,7 +69,7 @@ class QFactory(object): # i.e. a question object (radio, checkbox, ...). # ----------------------------------------------------------------------- # async def generate_async(self): - # loop = asyncio.get_event_loop() + # loop = asyncio.get_running_loop() # return await loop.run_in_executor(None, self.generate) def generate(self): diff --git a/aprendizations/knowledge.py b/aprendizations/knowledge.py index 3f7d99a..1adc8f0 100644 --- a/aprendizations/knowledge.py +++ b/aprendizations/knowledge.py @@ -43,34 +43,33 @@ class StudentKnowledge(object): now = datetime.now() for tref, s in self.state.items(): dt = now - s['date'] - s['level'] *= 0.95 ** dt.days # forgetting factor 0.95 FIXME + s['level'] *= 0.98 ** dt.days # forgetting factor 0.95 FIXME # ------------------------------------------------------------------------ # Unlock topics whose dependencies are satisfied (> min_level) # ------------------------------------------------------------------------ def unlock_topics(self): - # minimum level that the dependencies of a topic must have - # for the topic to be unlocked. - min_level = 0.01 - for topic in self.deps.nodes(): if topic not in self.state: # if locked pred = self.deps.predecessors(topic) + min_level = self.deps.node[topic]['min_level'] if all(d in self.state and self.state[d]['level'] > min_level - for d in pred): # all dependencies are done + for d in pred): # all deps are greater than min_level self.state[topic] = { 'level': 0.0, # unlocked 'date': datetime.now() } logger.debug(f'Unlocked "{topic}".') + # else: # lock this topic if deps do not satisfy min_level + # del self.state[topic] # ------------------------------------------------------------------------ # Start a new topic. # questions: list of generated questions to do in the topic # current_question: the current question to be presented # ------------------------------------------------------------------------ - # FIXME async mas nao tem awaits... + # FIXME async mas nao tem awaits... do not allow restart same topic async def start_topic(self, topic): logger.debug(f'StudentKnowledge.start_topic({topic})') if self.current_topic == topic: @@ -88,7 +87,7 @@ class StudentKnowledge(object): t = self.deps.node[topic] k = t['choose'] - if t['shuffle']: + if t['shuffle_questions']: questions = random.sample(t['questions'], k=k) else: questions = t['questions'][:k] @@ -131,11 +130,10 @@ class StudentKnowledge(object): q = self.current_question q['answer'] = answer q['finish_time'] = datetime.now() - grade = await q.correct_async() - - logger.debug(f'Grade {grade:.2} ({q["ref"]})') + await q.correct_async() + logger.debug(f'Grade {q["grade"]:.2} ({q["ref"]})') - if grade > 0.999: + if q['grade'] > 0.999: self.correct_answers += 1 self.next_question() action = 'right' diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index dc193b8..2e24338 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -26,8 +26,7 @@ logger = logging.getLogger(__name__) # helper functions # ============================================================================ async def _bcrypt_hash(a, b): - # loop = asyncio.get_running_loop() # FIXME python 3.7 only - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() return await loop.run_in_executor(None, bcrypt.hashpw, a.encode('utf-8'), b) @@ -248,11 +247,12 @@ class LearnApp(object): # default attributes that apply to the topics default_file = config.get('file', 'questions.yaml') - default_shuffle = config.get('shuffle', True) + default_shuffle_questions = config.get('shuffle_questions', True) default_choose = config.get('choose', 9999) default_forgetting_factor = config.get('forgetting_factor', 1.0) default_maxtries = config.get('max_tries', 3) default_append_wrong = config.get('append_wrong', True) + default_min_level = config.get('min_level', 0.01) # to unlock topic # iterate over topics and populate graph topics = config.get('topics', {}) @@ -267,10 +267,11 @@ class LearnApp(object): t['name'] = attr.get('name', tref) t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic t['file'] = attr.get('file', default_file) # questions.yaml - t['shuffle'] = attr.get('shuffle', default_shuffle) + t['shuffle_questions'] = attr.get('shuffle_questions', default_shuffle_questions) t['max_tries'] = attr.get('max_tries', default_maxtries) t['forgetting_factor'] = attr.get('forgetting_factor', default_forgetting_factor) + t['min_level'] = attr.get('min_level', default_min_level) t['choose'] = attr.get('choose', default_choose) t['append_wrong'] = attr.get('append_wrong', default_append_wrong) t['questions'] = attr.get('questions', []) diff --git a/aprendizations/questions.py b/aprendizations/questions.py index 5de9374..0bd37f7 100644 --- a/aprendizations/questions.py +++ b/aprendizations/questions.py @@ -43,17 +43,15 @@ class Question(dict): 'files': {}, }) - def correct(self) -> float: + def correct(self) -> None: self['comments'] = '' self['grade'] = 0.0 - return 0.0 - async def correct_async(self) -> float: - loop = asyncio.get_running_loop() # FIXME python 3.7 only - # loop = asyncio.get_event_loop() # python 3.6 - return await loop.run_in_executor(None, self.correct) + async def correct_async(self) -> None: + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, self.correct) - def set_defaults(self, d): + def set_defaults(self, d) -> None: 'Add k:v pairs from default dict d for nonexistent keys' for k, v in d.items(): self.setdefault(k, v) @@ -204,8 +202,6 @@ class QuestionCheckbox(Question): self['grade'] = x / sum_abs - return self['grade'] - # =========================================================================== class QuestionText(Question): @@ -239,8 +235,6 @@ class QuestionText(Question): if self['answer'] is not None: self['grade'] = 1.0 if self['answer'] in self['correct'] else 0.0 - return self['grade'] - # =========================================================================== class QuestionTextRegex(Question): @@ -271,8 +265,6 @@ class QuestionTextRegex(Question): f'answer {self["answer"]}.') self['grade'] = 1.0 if ok else 0.0 - return self['grade'] - # =========================================================================== class QuestionNumericInterval(Question): @@ -308,8 +300,6 @@ class QuestionNumericInterval(Question): else: self['grade'] = 1.0 if lower <= answer <= upper else 0.0 - return self['grade'] - # =========================================================================== class QuestionTextArea(Question): @@ -358,8 +348,6 @@ class QuestionTextArea(Question): else: self['grade'] = float(out) - return self['grade'] - # =========================================================================== class QuestionInformation(Question): @@ -375,4 +363,3 @@ class QuestionInformation(Question): def correct(self): super().correct() self['grade'] = 1.0 # always "correct" but points should be zero! - return self['grade'] diff --git a/aprendizations/serve.py b/aprendizations/serve.py index 8b04785..cd7a7ec 100644 --- a/aprendizations/serve.py +++ b/aprendizations/serve.py @@ -95,7 +95,7 @@ class LoginHandler(BaseHandler): self.render('login.html', error='') async def post(self): - uid = self.get_body_argument('uid') + uid = self.get_body_argument('uid').lstrip('l') pw = self.get_body_argument('pw') login_ok = await self.learn.login(uid, pw) diff --git a/aprendizations/static/css/topic.css b/aprendizations/static/css/topic.css index 90db3ed..01aae8e 100644 --- a/aprendizations/static/css/topic.css +++ b/aprendizations/static/css/topic.css @@ -23,6 +23,7 @@ html { position: relative; min-height: 100%; } -textarea { - font-family: monospace; +.CodeMirror { + border: 1px solid #eee; + height: auto; } diff --git a/aprendizations/templates/question-textarea.html b/aprendizations/templates/question-textarea.html index 8a9c435..895dcbf 100644 --- a/aprendizations/templates/question-textarea.html +++ b/aprendizations/templates/question-textarea.html @@ -7,10 +7,9 @@ diff --git a/aprendizations/templates/topic.html b/aprendizations/templates/topic.html index 9a47685..911b930 100644 --- a/aprendizations/templates/topic.html +++ b/aprendizations/templates/topic.html @@ -32,8 +32,8 @@ - - + + diff --git a/aprendizations/tools.py b/aprendizations/tools.py index 9f10cc5..2d7d727 100644 --- a/aprendizations/tools.py +++ b/aprendizations/tools.py @@ -147,7 +147,7 @@ def load_yaml(filename, default=None): else: with f: try: - default = yaml.load(f) + default = yaml.safe_load(f) # FIXME check if supports all kinds of questions including regex except yaml.YAMLError as e: mark = e.problem_mark logger.error(f'In file "{filename}" near line {mark.line}, ' @@ -187,7 +187,7 @@ def run_script(script, args=[], stdin='', timeout=5): logger.error(f'Return code {p.returncode} running "{script}".') else: try: - output = yaml.load(p.stdout) + output = yaml.safe_load(p.stdout) except Exception: logger.error(f'Error parsing yaml output of "{script}"') else: diff --git a/demo/demo.yaml b/demo/demo.yaml index 2298e93..ec92143 100644 --- a/demo/demo.yaml +++ b/demo/demo.yaml @@ -5,12 +5,13 @@ database: students.db # values applie to each topic, if undefined there -# default values are: file=question.yaml, shuffle=True, choose: all file: questions.yaml -shuffle: false +shuffle_questions: true choose: 6 max_tries: 2 -forgetting_factor: 0.99 +forgetting_factor: 0.97 +min_level: 0.01 +append_wrong: true # ---------------------------------------------------------------------------- topics: -- libgit2 0.21.2