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: | ... | ... |