Commit a51bcefdf1c7771cdaec4595f2c398a443d25fbb

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

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).
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
@@ -23,6 +23,7 @@ html { @@ -23,6 +23,7 @@ html {
23 position: relative; 23 position: relative;
24 min-height: 100%; 24 min-height: 100%;
25 } 25 }
26 -textarea {  
27 - font-family: monospace; 26 +.CodeMirror {
  27 + border: 1px solid #eee;
  28 + height: auto;
28 } 29 }
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=&#39;&#39;, timeout=5): @@ -187,7 +187,7 @@ def run_script(script, args=[], stdin=&#39;&#39;, 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: