Commit fa091c84af26c006bf1964d0972a737d737c23ee

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

Allow generator to generate arguments to correction script of textarea.

Arguments are no longer sent to stdin, but in the command line instead.
Topic sequence shows all the unlocked topics first.
Drop compatibility with python 3.6 because of asyncio.get_running_loop().
@@ -10,6 +10,7 @@ @@ -10,6 +10,7 @@
10 10
11 # TODO 11 # TODO
12 12
  13 +- ao fim de 3 tentativas de login, envial email para aluno com link para definir nova password (com timeout de 5 minutos).
13 - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias. 14 - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias.
14 - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos. 15 - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos.
15 - botão não sei... 16 - botão não sei...
aprendizations/factory.py
@@ -83,9 +83,10 @@ class QFactory(object): @@ -83,9 +83,10 @@ class QFactory(object):
83 # output is then yaml parsed into a dictionary `q`. 83 # output is then yaml parsed into a dictionary `q`.
84 if q['type'] == 'generator': 84 if q['type'] == 'generator':
85 logger.debug(f' \\_ Running "{q["script"]}".') 85 logger.debug(f' \\_ Running "{q["script"]}".')
86 - q.setdefault('arg', '') # optional arguments will be sent to stdin 86 + q.setdefault('args', [])
  87 + q.setdefault('stdin', '')
87 script = path.join(q['path'], q['script']) 88 script = path.join(q['path'], q['script'])
88 - out = run_script(script=script, stdin=q['arg']) 89 + out = run_script(script=script, args=q['args'], stdin=q['stdin'])
89 q.update(out) 90 q.update(out)
90 91
91 # Finally we create an instance of Question() 92 # Finally we create an instance of Question()
aprendizations/knowledge.py
@@ -31,8 +31,8 @@ class StudentKnowledge(object): @@ -31,8 +31,8 @@ class StudentKnowledge(object):
31 self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} 31 self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...}
32 32
33 self.update_topic_levels() # applies forgetting factor 33 self.update_topic_levels() # applies forgetting factor
34 - self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...]  
35 self.unlock_topics() # whose dependencies have been completed 34 self.unlock_topics() # whose dependencies have been completed
  35 + self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...]
36 self.current_topic = None 36 self.current_topic = None
37 37
38 # ------------------------------------------------------------------------ 38 # ------------------------------------------------------------------------
@@ -53,7 +53,7 @@ class StudentKnowledge(object): @@ -53,7 +53,7 @@ class StudentKnowledge(object):
53 # for the topic to be unlocked. 53 # for the topic to be unlocked.
54 min_level = 0.01 54 min_level = 0.01
55 55
56 - for topic in self.topic_sequence: 56 + for topic in self.deps.nodes():
57 if topic not in self.state: # if locked 57 if topic not in self.state: # if locked
58 pred = self.deps.predecessors(topic) 58 pred = self.deps.predecessors(topic)
59 if all(d in self.state and self.state[d]['level'] > min_level 59 if all(d in self.state and self.state[d]['level'] > min_level
@@ -185,19 +185,10 @@ class StudentKnowledge(object): @@ -185,19 +185,10 @@ class StudentKnowledge(object):
185 # compute recommended sequence of topics ['a', 'b', ...] 185 # compute recommended sequence of topics ['a', 'b', ...]
186 # ------------------------------------------------------------------------ 186 # ------------------------------------------------------------------------
187 def recommend_topic_sequence(self, target=None): 187 def recommend_topic_sequence(self, target=None):
188 - return list(nx.topological_sort(self.deps))  
189 -  
190 - # if target is None:  
191 - # target = list(nx.topological_sort(self.deps))[-1]  
192 -  
193 - # weights = {}  
194 - # tseq = [target]  
195 -  
196 - # def topic_weights(d, g, n):  
197 - # pred = g.predecessors(n)  
198 - # for p in pred:  
199 - # topic_weights(d, g, p)  
200 - # d[n] = 1 + sum(d[m]) 188 + tt = list(nx.topological_sort(self.deps))
  189 + unlocked = [t for t in tt if t in self.state]
  190 + locked = [t for t in tt if t not in unlocked]
  191 + return unlocked + locked
201 192
202 # ------------------------------------------------------------------------ 193 # ------------------------------------------------------------------------
203 def get_current_question(self): 194 def get_current_question(self):
aprendizations/questions.py
@@ -49,10 +49,9 @@ class Question(dict): @@ -49,10 +49,9 @@ class Question(dict):
49 return 0.0 49 return 0.0
50 50
51 async def correct_async(self) -> float: 51 async def correct_async(self) -> float:
52 - # loop = asyncio.get_running_loop() # FIXME python 3.7 only  
53 - loop = asyncio.get_event_loop()  
54 - grade = await loop.run_in_executor(None, self.correct)  
55 - return grade 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)
56 55
57 def set_defaults(self, d): 56 def set_defaults(self, d):
58 'Add k:v pairs from default dict d for nonexistent keys' 57 'Add k:v pairs from default dict d for nonexistent keys'
@@ -234,7 +233,6 @@ class QuestionText(Question): @@ -234,7 +233,6 @@ class QuestionText(Question):
234 self['correct'] = [str(a) for a in self['correct']] 233 self['correct'] = [str(a) for a in self['correct']]
235 234
236 # ------------------------------------------------------------------------ 235 # ------------------------------------------------------------------------
237 - # can return negative values for wrong answers  
238 def correct(self): 236 def correct(self):
239 super().correct() 237 super().correct()
240 238
@@ -263,7 +261,6 @@ class QuestionTextRegex(Question): @@ -263,7 +261,6 @@ class QuestionTextRegex(Question):
263 }) 261 })
264 262
265 # ------------------------------------------------------------------------ 263 # ------------------------------------------------------------------------
266 - # can return negative values for wrong answers  
267 def correct(self): 264 def correct(self):
268 super().correct() 265 super().correct()
269 if self['answer'] is not None: 266 if self['answer'] is not None:
@@ -297,7 +294,6 @@ class QuestionNumericInterval(Question): @@ -297,7 +294,6 @@ class QuestionNumericInterval(Question):
297 }) 294 })
298 295
299 # ------------------------------------------------------------------------ 296 # ------------------------------------------------------------------------
300 - # can return negative values for wrong answers  
301 def correct(self): 297 def correct(self):
302 super().correct() 298 super().correct()
303 if self['answer'] is not None: 299 if self['answer'] is not None:
@@ -333,20 +329,20 @@ class QuestionTextArea(Question): @@ -333,20 +329,20 @@ class QuestionTextArea(Question):
333 'text': '', 329 'text': '',
334 'lines': 8, 330 'lines': 8,
335 'timeout': 5, # seconds 331 'timeout': 5, # seconds
336 - 'correct': '' # trying to execute this will fail => grade 0.0 332 + 'correct': '', # trying to execute this will fail => grade 0.0
  333 + 'args': []
337 }) 334 })
338 335
339 self['correct'] = path.join(self['path'], self['correct']) # FIXME 336 self['correct'] = path.join(self['path'], self['correct']) # FIXME
340 337
341 # ------------------------------------------------------------------------ 338 # ------------------------------------------------------------------------
342 - # can return negative values for wrong answers  
343 def correct(self): 339 def correct(self):
344 super().correct() 340 super().correct()
345 341
346 - if self['answer'] is not None:  
347 - # correct answer  
348 - out = run_script( # and parse yaml ouput 342 + if self['answer'] is not None: # correct answer and parse yaml ouput
  343 + out = run_script(
349 script=self['correct'], 344 script=self['correct'],
  345 + args=self['args'],
350 stdin=self['answer'], 346 stdin=self['answer'],
351 timeout=self['timeout'] 347 timeout=self['timeout']
352 ) 348 )
aprendizations/templates/maintopics-table.html
@@ -102,6 +102,7 @@ @@ -102,6 +102,7 @@
102 {% elif t['type']=='chapter' %} 102 {% elif t['type']=='chapter' %}
103 <div class="text-xs-right"> 103 <div class="text-xs-right">
104 <i class="fas fa-trophy fa-lg text-warning"></i> 104 <i class="fas fa-trophy fa-lg text-warning"></i>
  105 + <!-- <i class="fas fa-award fa-lg text-warning"></i> -->
105 </div> 106 </div>
106 {% else %} 107 {% else %}
107 <div class="text-xs-right"> 108 <div class="text-xs-right">
aprendizations/tools.py
@@ -161,10 +161,11 @@ def load_yaml(filename, default=None): @@ -161,10 +161,11 @@ def load_yaml(filename, default=None):
161 # The script is run in another process but this function blocks waiting 161 # The script is run in another process but this function blocks waiting
162 # for its termination. 162 # for its termination.
163 # --------------------------------------------------------------------------- 163 # ---------------------------------------------------------------------------
164 -def run_script(script, stdin='', timeout=5): 164 +def run_script(script, args=[], stdin='', timeout=5):
165 script = path.expanduser(script) 165 script = path.expanduser(script)
166 try: 166 try:
167 - p = subprocess.run([script], 167 + cmd = [script] + [str(a) for a in args]
  168 + p = subprocess.run(cmd,
168 input=stdin, 169 input=stdin,
169 stdout=subprocess.PIPE, 170 stdout=subprocess.PIPE,
170 stderr=subprocess.STDOUT, 171 stderr=subprocess.STDOUT,
@@ -179,6 +180,8 @@ def run_script(script, stdin=&#39;&#39;, timeout=5): @@ -179,6 +180,8 @@ def run_script(script, stdin=&#39;&#39;, timeout=5):
179 logger.error(f'Can not execute script "{script}": unknown reason.') 180 logger.error(f'Can not execute script "{script}": unknown reason.')
180 except subprocess.TimeoutExpired: 181 except subprocess.TimeoutExpired:
181 logger.error(f'Timeout {timeout}s exceeded while running "{script}".') 182 logger.error(f'Timeout {timeout}s exceeded while running "{script}".')
  183 + except Exception:
  184 + logger.error(f'An Exception ocurred running {script}.')
182 else: 185 else:
183 if p.returncode != 0: 186 if p.returncode != 0:
184 logger.error(f'Return code {p.returncode} running "{script}".') 187 logger.error(f'Return code {p.returncode} running "{script}".')
demo/math/gen-multiples-of-3.py
@@ -3,9 +3,8 @@ @@ -3,9 +3,8 @@
3 import random 3 import random
4 import sys 4 import sys
5 5
6 -arg = sys.stdin.read() # read arguments 6 +a, b = map(int, sys.argv[1:]) # get command line arguments
7 7
8 -a,b = (int(n) for n in arg.split(','))  
9 numbers = list(range(a, b)) 8 numbers = list(range(a, b))
10 random.shuffle(numbers) 9 random.shuffle(numbers)
11 numbers = numbers[:8] 10 numbers = numbers[:8]
@@ -18,14 +17,13 @@ else: @@ -18,14 +17,13 @@ else:
18 solution = f'Nenhum número mostrado é múltiplo de 3.' 17 solution = f'Nenhum número mostrado é múltiplo de 3.'
19 18
20 19
21 -q = f''' 20 +q = f'''---
22 type: checkbox 21 type: checkbox
23 title: Múltiplos de 3 22 title: Múltiplos de 3
24 text: Indique quais dos seguintes números são múltiplos de 3. 23 text: Indique quais dos seguintes números são múltiplos de 3.
25 options: {numbers} 24 options: {numbers}
26 correct: {correct} 25 correct: {correct}
27 solution: | 26 solution: |
28 - {solution}  
29 -''' 27 + {solution}'''
30 28
31 print(q) 29 print(q)
demo/math/questions.yaml
@@ -79,6 +79,6 @@ @@ -79,6 +79,6 @@
79 - type: generator 79 - type: generator
80 ref: overflow 80 ref: overflow
81 script: gen-multiples-of-3.py 81 script: gen-multiples-of-3.py
82 - arg: "11,120"  
83 - # the program should print a question in yaml format. The arg will be  
84 - # sent to the stdin when the program is run. 82 + args: [11, 120]
  83 + # the program should print a question in yaml format. The args will be
  84 + # sent as command line options when the program is run.