Commit 0b1675b0ba146a3513d7df2633f189ccd7282e4e
1 parent
db2aceed
Exists in
master
and in
1 other branch
- 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
Showing
7 changed files
with
57 additions
and
42 deletions
Show diff stats
BUGS.md
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. |
app.py
@@ -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(): |
knowledge.py
@@ -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 |
questions.py
@@ -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 |
serve.py
@@ -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 |