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 | # BUGS | 2 | # BUGS |
3 | 3 | ||
4 | +- testar se perguntas regex funcionam com yaml.safe_load. | ||
4 | - 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 | - 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 | - click numa opcao checkbox fora da checkbox+label não está a funcionar. | 6 | - click numa opcao checkbox fora da checkbox+label não está a funcionar. |
6 | - shift-enter não está a funcionar | 7 | - shift-enter não está a funcionar |
aprendizations/factory.py
@@ -48,17 +48,17 @@ class QFactory(object): | @@ -48,17 +48,17 @@ class QFactory(object): | ||
48 | # Depending on the type of question, a different question class will be | 48 | # Depending on the type of question, a different question class will be |
49 | # instantiated. All these classes derive from the base class `Question`. | 49 | # instantiated. All these classes derive from the base class `Question`. |
50 | _types = { | 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 | 'numeric-interval': QuestionNumericInterval, | 55 | 'numeric-interval': QuestionNumericInterval, |
56 | - 'textarea': QuestionTextArea, | 56 | + 'textarea': QuestionTextArea, |
57 | # -- informative panels -- | 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 | def __init__(self, question_dict={}): | 64 | def __init__(self, question_dict={}): |
@@ -69,7 +69,7 @@ class QFactory(object): | @@ -69,7 +69,7 @@ class QFactory(object): | ||
69 | # i.e. a question object (radio, checkbox, ...). | 69 | # i.e. a question object (radio, checkbox, ...). |
70 | # ----------------------------------------------------------------------- | 70 | # ----------------------------------------------------------------------- |
71 | # async def generate_async(self): | 71 | # async def generate_async(self): |
72 | - # loop = asyncio.get_event_loop() | 72 | + # loop = asyncio.get_running_loop() |
73 | # return await loop.run_in_executor(None, self.generate) | 73 | # return await loop.run_in_executor(None, self.generate) |
74 | 74 | ||
75 | def generate(self): | 75 | def generate(self): |
aprendizations/knowledge.py
@@ -43,34 +43,33 @@ class StudentKnowledge(object): | @@ -43,34 +43,33 @@ class StudentKnowledge(object): | ||
43 | now = datetime.now() | 43 | now = datetime.now() |
44 | for tref, s in self.state.items(): | 44 | for tref, s in self.state.items(): |
45 | dt = now - s['date'] | 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 | # Unlock topics whose dependencies are satisfied (> min_level) | 49 | # Unlock topics whose dependencies are satisfied (> min_level) |
50 | # ------------------------------------------------------------------------ | 50 | # ------------------------------------------------------------------------ |
51 | def unlock_topics(self): | 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 | for topic in self.deps.nodes(): | 52 | for topic in self.deps.nodes(): |
57 | if topic not in self.state: # if locked | 53 | if topic not in self.state: # if locked |
58 | pred = self.deps.predecessors(topic) | 54 | pred = self.deps.predecessors(topic) |
55 | + min_level = self.deps.node[topic]['min_level'] | ||
59 | if all(d in self.state and self.state[d]['level'] > min_level | 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 | self.state[topic] = { | 59 | self.state[topic] = { |
63 | 'level': 0.0, # unlocked | 60 | 'level': 0.0, # unlocked |
64 | 'date': datetime.now() | 61 | 'date': datetime.now() |
65 | } | 62 | } |
66 | logger.debug(f'Unlocked "{topic}".') | 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 | # Start a new topic. | 68 | # Start a new topic. |
70 | # questions: list of generated questions to do in the topic | 69 | # questions: list of generated questions to do in the topic |
71 | # current_question: the current question to be presented | 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 | async def start_topic(self, topic): | 73 | async def start_topic(self, topic): |
75 | logger.debug(f'StudentKnowledge.start_topic({topic})') | 74 | logger.debug(f'StudentKnowledge.start_topic({topic})') |
76 | if self.current_topic == topic: | 75 | if self.current_topic == topic: |
@@ -88,7 +87,7 @@ class StudentKnowledge(object): | @@ -88,7 +87,7 @@ class StudentKnowledge(object): | ||
88 | 87 | ||
89 | t = self.deps.node[topic] | 88 | t = self.deps.node[topic] |
90 | k = t['choose'] | 89 | k = t['choose'] |
91 | - if t['shuffle']: | 90 | + if t['shuffle_questions']: |
92 | questions = random.sample(t['questions'], k=k) | 91 | questions = random.sample(t['questions'], k=k) |
93 | else: | 92 | else: |
94 | questions = t['questions'][:k] | 93 | questions = t['questions'][:k] |
@@ -131,11 +130,10 @@ class StudentKnowledge(object): | @@ -131,11 +130,10 @@ class StudentKnowledge(object): | ||
131 | q = self.current_question | 130 | q = self.current_question |
132 | q['answer'] = answer | 131 | q['answer'] = answer |
133 | q['finish_time'] = datetime.now() | 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 | self.correct_answers += 1 | 137 | self.correct_answers += 1 |
140 | self.next_question() | 138 | self.next_question() |
141 | action = 'right' | 139 | action = 'right' |
aprendizations/learnapp.py
@@ -26,8 +26,7 @@ logger = logging.getLogger(__name__) | @@ -26,8 +26,7 @@ logger = logging.getLogger(__name__) | ||
26 | # helper functions | 26 | # helper functions |
27 | # ============================================================================ | 27 | # ============================================================================ |
28 | async def _bcrypt_hash(a, b): | 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 | return await loop.run_in_executor(None, bcrypt.hashpw, | 30 | return await loop.run_in_executor(None, bcrypt.hashpw, |
32 | a.encode('utf-8'), b) | 31 | a.encode('utf-8'), b) |
33 | 32 | ||
@@ -248,11 +247,12 @@ class LearnApp(object): | @@ -248,11 +247,12 @@ class LearnApp(object): | ||
248 | 247 | ||
249 | # default attributes that apply to the topics | 248 | # default attributes that apply to the topics |
250 | default_file = config.get('file', 'questions.yaml') | 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 | default_choose = config.get('choose', 9999) | 251 | default_choose = config.get('choose', 9999) |
253 | default_forgetting_factor = config.get('forgetting_factor', 1.0) | 252 | default_forgetting_factor = config.get('forgetting_factor', 1.0) |
254 | default_maxtries = config.get('max_tries', 3) | 253 | default_maxtries = config.get('max_tries', 3) |
255 | default_append_wrong = config.get('append_wrong', True) | 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 | # iterate over topics and populate graph | 257 | # iterate over topics and populate graph |
258 | topics = config.get('topics', {}) | 258 | topics = config.get('topics', {}) |
@@ -267,10 +267,11 @@ class LearnApp(object): | @@ -267,10 +267,11 @@ class LearnApp(object): | ||
267 | t['name'] = attr.get('name', tref) | 267 | t['name'] = attr.get('name', tref) |
268 | t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic | 268 | t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic |
269 | t['file'] = attr.get('file', default_file) # questions.yaml | 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 | t['max_tries'] = attr.get('max_tries', default_maxtries) | 271 | t['max_tries'] = attr.get('max_tries', default_maxtries) |
272 | t['forgetting_factor'] = attr.get('forgetting_factor', | 272 | t['forgetting_factor'] = attr.get('forgetting_factor', |
273 | default_forgetting_factor) | 273 | default_forgetting_factor) |
274 | + t['min_level'] = attr.get('min_level', default_min_level) | ||
274 | t['choose'] = attr.get('choose', default_choose) | 275 | t['choose'] = attr.get('choose', default_choose) |
275 | t['append_wrong'] = attr.get('append_wrong', default_append_wrong) | 276 | t['append_wrong'] = attr.get('append_wrong', default_append_wrong) |
276 | t['questions'] = attr.get('questions', []) | 277 | t['questions'] = attr.get('questions', []) |
aprendizations/questions.py
@@ -43,17 +43,15 @@ class Question(dict): | @@ -43,17 +43,15 @@ class Question(dict): | ||
43 | 'files': {}, | 43 | 'files': {}, |
44 | }) | 44 | }) |
45 | 45 | ||
46 | - def correct(self) -> float: | 46 | + def correct(self) -> None: |
47 | self['comments'] = '' | 47 | self['comments'] = '' |
48 | self['grade'] = 0.0 | 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 | 'Add k:v pairs from default dict d for nonexistent keys' | 55 | 'Add k:v pairs from default dict d for nonexistent keys' |
58 | for k, v in d.items(): | 56 | for k, v in d.items(): |
59 | self.setdefault(k, v) | 57 | self.setdefault(k, v) |
@@ -204,8 +202,6 @@ class QuestionCheckbox(Question): | @@ -204,8 +202,6 @@ class QuestionCheckbox(Question): | ||
204 | 202 | ||
205 | self['grade'] = x / sum_abs | 203 | self['grade'] = x / sum_abs |
206 | 204 | ||
207 | - return self['grade'] | ||
208 | - | ||
209 | 205 | ||
210 | # =========================================================================== | 206 | # =========================================================================== |
211 | class QuestionText(Question): | 207 | class QuestionText(Question): |
@@ -239,8 +235,6 @@ class QuestionText(Question): | @@ -239,8 +235,6 @@ class QuestionText(Question): | ||
239 | if self['answer'] is not None: | 235 | if self['answer'] is not None: |
240 | self['grade'] = 1.0 if self['answer'] in self['correct'] else 0.0 | 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 | class QuestionTextRegex(Question): | 240 | class QuestionTextRegex(Question): |
@@ -271,8 +265,6 @@ class QuestionTextRegex(Question): | @@ -271,8 +265,6 @@ class QuestionTextRegex(Question): | ||
271 | f'answer {self["answer"]}.') | 265 | f'answer {self["answer"]}.') |
272 | self['grade'] = 1.0 if ok else 0.0 | 266 | self['grade'] = 1.0 if ok else 0.0 |
273 | 267 | ||
274 | - return self['grade'] | ||
275 | - | ||
276 | 268 | ||
277 | # =========================================================================== | 269 | # =========================================================================== |
278 | class QuestionNumericInterval(Question): | 270 | class QuestionNumericInterval(Question): |
@@ -308,8 +300,6 @@ class QuestionNumericInterval(Question): | @@ -308,8 +300,6 @@ class QuestionNumericInterval(Question): | ||
308 | else: | 300 | else: |
309 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 | 301 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 |
310 | 302 | ||
311 | - return self['grade'] | ||
312 | - | ||
313 | 303 | ||
314 | # =========================================================================== | 304 | # =========================================================================== |
315 | class QuestionTextArea(Question): | 305 | class QuestionTextArea(Question): |
@@ -358,8 +348,6 @@ class QuestionTextArea(Question): | @@ -358,8 +348,6 @@ class QuestionTextArea(Question): | ||
358 | else: | 348 | else: |
359 | self['grade'] = float(out) | 349 | self['grade'] = float(out) |
360 | 350 | ||
361 | - return self['grade'] | ||
362 | - | ||
363 | 351 | ||
364 | # =========================================================================== | 352 | # =========================================================================== |
365 | class QuestionInformation(Question): | 353 | class QuestionInformation(Question): |
@@ -375,4 +363,3 @@ class QuestionInformation(Question): | @@ -375,4 +363,3 @@ class QuestionInformation(Question): | ||
375 | def correct(self): | 363 | def correct(self): |
376 | super().correct() | 364 | super().correct() |
377 | self['grade'] = 1.0 # always "correct" but points should be zero! | 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,7 +95,7 @@ class LoginHandler(BaseHandler): | ||
95 | self.render('login.html', error='') | 95 | self.render('login.html', error='') |
96 | 96 | ||
97 | async def post(self): | 97 | async def post(self): |
98 | - uid = self.get_body_argument('uid') | 98 | + uid = self.get_body_argument('uid').lstrip('l') |
99 | pw = self.get_body_argument('pw') | 99 | pw = self.get_body_argument('pw') |
100 | 100 | ||
101 | login_ok = await self.learn.login(uid, pw) | 101 | login_ok = await self.learn.login(uid, pw) |
aprendizations/static/css/topic.css
aprendizations/templates/question-textarea.html
@@ -7,10 +7,9 @@ | @@ -7,10 +7,9 @@ | ||
7 | 7 | ||
8 | <script> | 8 | <script> |
9 | var editor = CodeMirror.fromTextArea(document.getElementById("code"), { | 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 | </script> | 14 | </script> |
16 | 15 |
aprendizations/templates/topic.html
@@ -32,8 +32,8 @@ | @@ -32,8 +32,8 @@ | ||
32 | <script defer src="/static/mdbootstrap/js/bootstrap.min.js"></script> | 32 | <script defer src="/static/mdbootstrap/js/bootstrap.min.js"></script> |
33 | <script defer src="/static/mdbootstrap/js/mdb.min.js"></script> | 33 | <script defer src="/static/mdbootstrap/js/mdb.min.js"></script> |
34 | <script defer src="/static/fontawesome-free/js/all.min.js"></script> | 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 | </head> | 38 | </head> |
39 | <!-- ===================================================================== --> | 39 | <!-- ===================================================================== --> |
aprendizations/tools.py
@@ -147,7 +147,7 @@ def load_yaml(filename, default=None): | @@ -147,7 +147,7 @@ def load_yaml(filename, default=None): | ||
147 | else: | 147 | else: |
148 | with f: | 148 | with f: |
149 | try: | 149 | try: |
150 | - default = yaml.load(f) | 150 | + default = yaml.safe_load(f) # FIXME check if supports all kinds of questions including regex |
151 | except yaml.YAMLError as e: | 151 | except yaml.YAMLError as e: |
152 | mark = e.problem_mark | 152 | mark = e.problem_mark |
153 | logger.error(f'In file "{filename}" near line {mark.line}, ' | 153 | logger.error(f'In file "{filename}" near line {mark.line}, ' |
@@ -187,7 +187,7 @@ def run_script(script, args=[], stdin='', timeout=5): | @@ -187,7 +187,7 @@ def run_script(script, args=[], stdin='', timeout=5): | ||
187 | logger.error(f'Return code {p.returncode} running "{script}".') | 187 | logger.error(f'Return code {p.returncode} running "{script}".') |
188 | else: | 188 | else: |
189 | try: | 189 | try: |
190 | - output = yaml.load(p.stdout) | 190 | + output = yaml.safe_load(p.stdout) |
191 | except Exception: | 191 | except Exception: |
192 | logger.error(f'Error parsing yaml output of "{script}"') | 192 | logger.error(f'Error parsing yaml output of "{script}"') |
193 | else: | 193 | else: |
demo/demo.yaml
@@ -5,12 +5,13 @@ database: students.db | @@ -5,12 +5,13 @@ database: students.db | ||
5 | 5 | ||
6 | 6 | ||
7 | # values applie to each topic, if undefined there | 7 | # values applie to each topic, if undefined there |
8 | -# default values are: file=question.yaml, shuffle=True, choose: all | ||
9 | file: questions.yaml | 8 | file: questions.yaml |
10 | -shuffle: false | 9 | +shuffle_questions: true |
11 | choose: 6 | 10 | choose: 6 |
12 | max_tries: 2 | 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 | topics: | 17 | topics: |