Commit 0b1675b0ba146a3513d7df2633f189ccd7282e4e

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

- fix browser redirection to /question when enter key is pressed.

- fix direction of graph edges.
- use config file given in a commandline option.
- improved debug messages
- removed panel from question html
1 BUGS: 1 BUGS:
2 2
3 -- de vez em quando o browser é redireccionado para /question em vez de fazer um post?? não percebo...  
4 -- load/save the knowledge state of the student 3 +- não entra à primeira
  4 +- logs mostram que está a gerar cada pergunta 2 vezes...??
  5 +- mostra tópicos do lado esquerdo, indicando quais estão feitos e quantas perguntas contêm.
5 - se students.db não existe, rebenta. 6 - se students.db não existe, rebenta.
6 - database hardcoded in LearnApp. 7 - database hardcoded in LearnApp.
7 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() 8 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]()
@@ -15,6 +16,8 @@ TODO: @@ -15,6 +16,8 @@ TODO:
15 16
16 SOLVED: 17 SOLVED:
17 18
  19 +- o browser é redireccionado para /question em vez de fazer um post?? quando se pressiona enter numa caixa text edit.
  20 +- load/save the knowledge state of the student
18 - servir ficheiros de public temporariamente 21 - servir ficheiros de public temporariamente
19 - path dos generators scripts mal construido 22 - path dos generators scripts mal construido
20 - questions hardcoded in LearnApp. 23 - questions hardcoded in LearnApp.
@@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
28 # LearnApp - application logic 28 # LearnApp - application logic
29 # ============================================================================ 29 # ============================================================================
30 class LearnApp(object): 30 class LearnApp(object):
31 - def __init__(self): 31 + def __init__(self, conffile='demo/demo.yaml'):
32 # online students 32 # online students
33 self.online = {} 33 self.online = {}
34 34
@@ -36,7 +36,7 @@ class LearnApp(object): @@ -36,7 +36,7 @@ class LearnApp(object):
36 self.db_setup('students.db') # FIXME 36 self.db_setup('students.db') # FIXME
37 37
38 # build dependency graph 38 # build dependency graph
39 - self.build_dependency_graph('demo/config.yaml') # FIXME 39 + self.build_dependency_graph(conffile) # FIXME
40 40
41 # add topics from depgraph to the database 41 # add topics from depgraph to the database
42 self.db_add_topics() 42 self.db_add_topics()
@@ -103,7 +103,7 @@ class LearnApp(object): @@ -103,7 +103,7 @@ class LearnApp(object):
103 103
104 # ------------------------------------------------------------------------ 104 # ------------------------------------------------------------------------
105 def get_current_public_dir(self, uid): 105 def get_current_public_dir(self, uid):
106 - topic = self.online[uid]['state'].topic 106 + topic = self.online[uid]['state'].get_current_topic()
107 p = self.depgraph.graph['path'] 107 p = self.depgraph.graph['path']
108 return path.join(p, topic, 'public') 108 return path.join(p, topic, 'public')
109 109
@@ -164,7 +164,7 @@ class LearnApp(object): @@ -164,7 +164,7 @@ class LearnApp(object):
164 # Build dependency graph 164 # Build dependency graph
165 deps = config.get('dependencies', {}) 165 deps = config.get('dependencies', {})
166 for n,dd in deps.items(): 166 for n,dd in deps.items():
167 - g.add_edges_from((n,d) for d in dd) 167 + g.add_edges_from((d,n) for d in dd)
168 168
169 # Builds factories for each node 169 # Builds factories for each node
170 for n in g.nodes_iter(): 170 for n in g.nodes_iter():
@@ -30,7 +30,10 @@ class Knowledge(object): @@ -30,7 +30,10 @@ class Knowledge(object):
30 def topic_generator(self): 30 def topic_generator(self):
31 topics = nx.topological_sort(self.depgraph) # FIXME for now... 31 topics = nx.topological_sort(self.depgraph) # FIXME for now...
32 for t in topics: 32 for t in topics:
  33 + if self.state.get(t, 0.0) > 0.999:
  34 + continue
33 self.questions = self.generate_questions_for_topic(t) 35 self.questions = self.generate_questions_for_topic(t)
  36 + logger.info(f'Generated {len(self.questions)} questions for topic "{t}"')
34 yield t 37 yield t
35 38
36 # ------------------------------------------------------------------------ 39 # ------------------------------------------------------------------------
@@ -43,6 +46,10 @@ class Knowledge(object): @@ -43,6 +46,10 @@ class Knowledge(object):
43 return self.current_question 46 return self.current_question
44 47
45 # ------------------------------------------------------------------------ 48 # ------------------------------------------------------------------------
  49 + def get_current_topic(self):
  50 + return self.current_topic
  51 +
  52 + # ------------------------------------------------------------------------
46 def get_knowledge_state(self): 53 def get_knowledge_state(self):
47 return self.state 54 return self.state
48 55
@@ -61,8 +68,6 @@ class Knowledge(object): @@ -61,8 +68,6 @@ class Knowledge(object):
61 self.current_topic = next(self.topic) 68 self.current_topic = next(self.topic)
62 self.questions = self.generate_questions_for_topic(self.current_topic) 69 self.questions = self.generate_questions_for_topic(self.current_topic)
63 70
64 - print(self.current_topic)  
65 - print(self.current_question)  
66 self.current_question = self.questions.pop(0) 71 self.current_question = self.questions.pop(0)
67 self.current_question['start_time'] = datetime.now() 72 self.current_question['start_time'] = datetime.now()
68 73
@@ -406,7 +406,7 @@ class QFactory(object): @@ -406,7 +406,7 @@ class QFactory(object):
406 # i.e. a question object (radio, checkbox, ...). 406 # i.e. a question object (radio, checkbox, ...).
407 # ----------------------------------------------------------------------- 407 # -----------------------------------------------------------------------
408 def generate(self): 408 def generate(self):
409 - logger.debug('generate()') 409 + logger.debug(f'generate "{self.question["ref"]}"')
410 # Shallow copy so that script generated questions will not replace 410 # Shallow copy so that script generated questions will not replace
411 # the original generators 411 # the original generators
412 q = self.question.copy() 412 q = self.question.copy()
@@ -415,10 +415,8 @@ class QFactory(object): @@ -415,10 +415,8 @@ class QFactory(object):
415 # which will print a valid question in yaml format to stdout. This 415 # which will print a valid question in yaml format to stdout. This
416 # output is then converted to a dictionary and `q` becomes that dict. 416 # output is then converted to a dictionary and `q` becomes that dict.
417 if q['type'] == 'generator': 417 if q['type'] == 'generator':
418 - logger.debug('Running script to generate question "{0}".'.format(q['ref'])) 418 + logger.debug(f'Running script "{q["script"]}"...')
419 q.setdefault('arg', '') # optional arguments will be sent to stdin 419 q.setdefault('arg', '') # optional arguments will be sent to stdin
420 - # print(q['path'])  
421 - # print(q['script'])  
422 script = path.join(q['path'], q['script']) 420 script = path.join(q['path'], q['script'])
423 out = run_script(script=script, stdin=q['arg']) 421 out = run_script(script=script, stdin=q['arg'])
424 q.update(out) 422 q.update(out)
@@ -433,18 +431,11 @@ class QFactory(object): @@ -433,18 +431,11 @@ class QFactory(object):
433 # }) 431 # })
434 432
435 # Finally we create an instance of Question() 433 # Finally we create an instance of Question()
436 - logger.debug('create instance...')  
437 - # try:  
438 - # qinstance = self._types[q['type']](q) # instance with correct class  
439 - # except KeyError as e:  
440 - # logger.error('Unknown question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref']))  
441 - # raise e  
442 - # except:  
443 - # logger.error('Failed to create question "{0}" from file "{1}".'.format(q['ref'], q['filename']))  
444 - # else:  
445 - # logger.debug('Generated question "{}".'.format(ref))  
446 - # return qinstance  
447 - qinstance = self._types[q['type']](q) # instance with correct class  
448 - logger.debug('returning')  
449 - return qinstance 434 + try:
  435 + qinstance = self._types[q['type']](q) # instance with correct class
  436 + except KeyError as e:
  437 + logger.error(f'Unknown question type "{q["type"]}"')
  438 + raise e
  439 + else:
  440 + return qinstance
450 441
@@ -8,6 +8,7 @@ import base64 @@ -8,6 +8,7 @@ import base64
8 import uuid 8 import uuid
9 import concurrent.futures 9 import concurrent.futures
10 import logging.config 10 import logging.config
  11 +import argparse
11 12
12 # user installed libraries 13 # user installed libraries
13 try: 14 try:
@@ -32,7 +33,7 @@ from tools import load_yaml, md @@ -32,7 +33,7 @@ from tools import load_yaml, md
32 # WebApplication - Tornado Web Server 33 # WebApplication - Tornado Web Server
33 # ============================================================================ 34 # ============================================================================
34 class WebApplication(tornado.web.Application): 35 class WebApplication(tornado.web.Application):
35 - def __init__(self): 36 + def __init__(self, learnapp):
36 handlers = [ 37 handlers = [
37 (r'/login', LoginHandler), 38 (r'/login', LoginHandler),
38 (r'/logout', LogoutHandler), 39 (r'/logout', LogoutHandler),
@@ -50,7 +51,7 @@ class WebApplication(tornado.web.Application): @@ -50,7 +51,7 @@ class WebApplication(tornado.web.Application):
50 'debug': True, 51 'debug': True,
51 } 52 }
52 super().__init__(handlers, **settings) 53 super().__init__(handlers, **settings)
53 - self.learn = LearnApp() 54 + self.learn = learnapp
54 55
55 # ============================================================================ 56 # ============================================================================
56 # Handlers 57 # Handlers
@@ -169,6 +170,16 @@ def main(): @@ -169,6 +170,16 @@ def main():
169 SERVER_PATH = os.path.dirname(os.path.realpath(__file__)) 170 SERVER_PATH = os.path.dirname(os.path.realpath(__file__))
170 LOGGER_CONF = os.path.join(SERVER_PATH, 'config/logger.yaml') 171 LOGGER_CONF = os.path.join(SERVER_PATH, 'config/logger.yaml')
171 172
  173 + # --- Commandline argument parsing
  174 + argparser = argparse.ArgumentParser(description='Server for online learning. Enrolled students and topics have to be previously configured. Please read the documentation included with this software before running the server.')
  175 + # FIXME:
  176 + # serverconf_file = path.normpath(path.join(SERVER_PATH, 'config', 'server.conf'))
  177 + # argparser.add_argument('--conf', default=serverconf_file, type=str, help='server configuration file')
  178 + # argparser.add_argument('--debug', action='store_true', help='Enable debug logging.')
  179 + # argparser.add_argument('--allow-all', action='store_true',
  180 + # help='Students are initially allowed to login (can be denied later)')
  181 + argparser.add_argument('conffile', type=str, nargs='+', help='Topics configuration file in YAML format.') # FIXME only one supported at the moment
  182 + arg = argparser.parse_args()
172 183
173 # --- Setup logging 184 # --- Setup logging
174 try: 185 try:
@@ -179,8 +190,9 @@ def main(): @@ -179,8 +190,9 @@ def main():
179 sys.exit(1) 190 sys.exit(1)
180 191
181 # --- start application 192 # --- start application
  193 + learnapp = LearnApp(arg.conffile[0])
182 try: 194 try:
183 - webapp = WebApplication() 195 + webapp = WebApplication(learnapp)
184 except Exception as e: 196 except Exception as e:
185 logging.critical('Can\'t start application.') 197 logging.critical('Can\'t start application.')
186 # sys.exit(1) 198 # sys.exit(1)
templates/learn.html
@@ -66,12 +66,12 @@ @@ -66,12 +66,12 @@
66 <!-- ===================================================================== --> 66 <!-- ===================================================================== -->
67 <!-- Container --> 67 <!-- Container -->
68 <div class="container"> 68 <div class="container">
69 -<audio> 69 +<!-- <audio>
70 <source id="snd-intro" src="/static/sounds/intro.mp3" type="audio/mpeg"> 70 <source id="snd-intro" src="/static/sounds/intro.mp3" type="audio/mpeg">
71 <source id="snd-correct" src="/static/sounds/correct.mp3" type="audio/mpeg"> 71 <source id="snd-correct" src="/static/sounds/correct.mp3" type="audio/mpeg">
72 <source id="snd-wrong" src="/static/sounds/wrong.mp3" type="audio/mpeg"> 72 <source id="snd-wrong" src="/static/sounds/wrong.mp3" type="audio/mpeg">
73 </audio> 73 </audio>
74 - 74 + -->
75 <form action="/question" method="post" id="question_form" autocomplete="off"> 75 <form action="/question" method="post" id="question_form" autocomplete="off">
76 {% module xsrf_form_html() %} 76 {% module xsrf_form_html() %}
77 77
@@ -106,9 +106,11 @@ function updateQuestion(response){ @@ -106,9 +106,11 @@ function updateQuestion(response){
106 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"question_div"]); 106 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"question_div"]);
107 107
108 $("textarea, input:text, input:radio, input:checkbox").keydown(function (e) { 108 $("textarea, input:text, input:radio, input:checkbox").keydown(function (e) {
109 - if (e.keyCode == 13 && e.shiftKey) { 109 + if (e.keyCode == 13) {
110 e.preventDefault(); 110 e.preventDefault();
111 - getQuestion(); 111 + if (e.shiftKey) {
  112 + getQuestion();
  113 + }
112 } 114 }
113 }); 115 });
114 // var audio = new Audio('/static/sounds/correct.mp3'); 116 // var audio = new Audio('/static/sounds/correct.mp3');
templates/question.html
1 {% autoescape %} 1 {% autoescape %}
2 2
3 -<div class="panel panel-default">  
4 - <div class="panel-body"> 3 +<!-- <div class="panel panel-default">
  4 + <div class="panel-body">
  5 + -->
  6 + <h3>{{ question['title'] }}</h3>
5 7
6 -<h3>{{ question['title'] }}</h3> 8 + <div id="text">
  9 + {{ md(question['text']) }}
  10 + </div>
7 11
8 -<div id="text">  
9 -{{ md(question['text']) }}  
10 -</div>  
11 -  
12 -{% block answer %}{% end %} 12 + {% block answer %}{% end %}
13 13
14 -</div></div>  
15 \ No newline at end of file 14 \ No newline at end of file
  15 +<!-- </div>
  16 +</div>
  17 + -->
16 \ No newline at end of file 18 \ No newline at end of file