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 | 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 | 6 | - se students.db não existe, rebenta. |
6 | 7 | - database hardcoded in LearnApp. |
7 | 8 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() |
... | ... | @@ -15,6 +16,8 @@ TODO: |
15 | 16 | |
16 | 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 | 21 | - servir ficheiros de public temporariamente |
19 | 22 | - path dos generators scripts mal construido |
20 | 23 | - questions hardcoded in LearnApp. | ... | ... |
app.py
... | ... | @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) |
28 | 28 | # LearnApp - application logic |
29 | 29 | # ============================================================================ |
30 | 30 | class LearnApp(object): |
31 | - def __init__(self): | |
31 | + def __init__(self, conffile='demo/demo.yaml'): | |
32 | 32 | # online students |
33 | 33 | self.online = {} |
34 | 34 | |
... | ... | @@ -36,7 +36,7 @@ class LearnApp(object): |
36 | 36 | self.db_setup('students.db') # FIXME |
37 | 37 | |
38 | 38 | # build dependency graph |
39 | - self.build_dependency_graph('demo/config.yaml') # FIXME | |
39 | + self.build_dependency_graph(conffile) # FIXME | |
40 | 40 | |
41 | 41 | # add topics from depgraph to the database |
42 | 42 | self.db_add_topics() |
... | ... | @@ -103,7 +103,7 @@ class LearnApp(object): |
103 | 103 | |
104 | 104 | # ------------------------------------------------------------------------ |
105 | 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 | 107 | p = self.depgraph.graph['path'] |
108 | 108 | return path.join(p, topic, 'public') |
109 | 109 | |
... | ... | @@ -164,7 +164,7 @@ class LearnApp(object): |
164 | 164 | # Build dependency graph |
165 | 165 | deps = config.get('dependencies', {}) |
166 | 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 | 169 | # Builds factories for each node |
170 | 170 | for n in g.nodes_iter(): | ... | ... |
knowledge.py
... | ... | @@ -30,7 +30,10 @@ class Knowledge(object): |
30 | 30 | def topic_generator(self): |
31 | 31 | topics = nx.topological_sort(self.depgraph) # FIXME for now... |
32 | 32 | for t in topics: |
33 | + if self.state.get(t, 0.0) > 0.999: | |
34 | + continue | |
33 | 35 | self.questions = self.generate_questions_for_topic(t) |
36 | + logger.info(f'Generated {len(self.questions)} questions for topic "{t}"') | |
34 | 37 | yield t |
35 | 38 | |
36 | 39 | # ------------------------------------------------------------------------ |
... | ... | @@ -43,6 +46,10 @@ class Knowledge(object): |
43 | 46 | return self.current_question |
44 | 47 | |
45 | 48 | # ------------------------------------------------------------------------ |
49 | + def get_current_topic(self): | |
50 | + return self.current_topic | |
51 | + | |
52 | + # ------------------------------------------------------------------------ | |
46 | 53 | def get_knowledge_state(self): |
47 | 54 | return self.state |
48 | 55 | |
... | ... | @@ -61,8 +68,6 @@ class Knowledge(object): |
61 | 68 | self.current_topic = next(self.topic) |
62 | 69 | self.questions = self.generate_questions_for_topic(self.current_topic) |
63 | 70 | |
64 | - print(self.current_topic) | |
65 | - print(self.current_question) | |
66 | 71 | self.current_question = self.questions.pop(0) |
67 | 72 | self.current_question['start_time'] = datetime.now() |
68 | 73 | ... | ... |
questions.py
... | ... | @@ -406,7 +406,7 @@ class QFactory(object): |
406 | 406 | # i.e. a question object (radio, checkbox, ...). |
407 | 407 | # ----------------------------------------------------------------------- |
408 | 408 | def generate(self): |
409 | - logger.debug('generate()') | |
409 | + logger.debug(f'generate "{self.question["ref"]}"') | |
410 | 410 | # Shallow copy so that script generated questions will not replace |
411 | 411 | # the original generators |
412 | 412 | q = self.question.copy() |
... | ... | @@ -415,10 +415,8 @@ class QFactory(object): |
415 | 415 | # which will print a valid question in yaml format to stdout. This |
416 | 416 | # output is then converted to a dictionary and `q` becomes that dict. |
417 | 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 | 419 | q.setdefault('arg', '') # optional arguments will be sent to stdin |
420 | - # print(q['path']) | |
421 | - # print(q['script']) | |
422 | 420 | script = path.join(q['path'], q['script']) |
423 | 421 | out = run_script(script=script, stdin=q['arg']) |
424 | 422 | q.update(out) |
... | ... | @@ -433,18 +431,11 @@ class QFactory(object): |
433 | 431 | # }) |
434 | 432 | |
435 | 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 | 8 | import uuid |
9 | 9 | import concurrent.futures |
10 | 10 | import logging.config |
11 | +import argparse | |
11 | 12 | |
12 | 13 | # user installed libraries |
13 | 14 | try: |
... | ... | @@ -32,7 +33,7 @@ from tools import load_yaml, md |
32 | 33 | # WebApplication - Tornado Web Server |
33 | 34 | # ============================================================================ |
34 | 35 | class WebApplication(tornado.web.Application): |
35 | - def __init__(self): | |
36 | + def __init__(self, learnapp): | |
36 | 37 | handlers = [ |
37 | 38 | (r'/login', LoginHandler), |
38 | 39 | (r'/logout', LogoutHandler), |
... | ... | @@ -50,7 +51,7 @@ class WebApplication(tornado.web.Application): |
50 | 51 | 'debug': True, |
51 | 52 | } |
52 | 53 | super().__init__(handlers, **settings) |
53 | - self.learn = LearnApp() | |
54 | + self.learn = learnapp | |
54 | 55 | |
55 | 56 | # ============================================================================ |
56 | 57 | # Handlers |
... | ... | @@ -169,6 +170,16 @@ def main(): |
169 | 170 | SERVER_PATH = os.path.dirname(os.path.realpath(__file__)) |
170 | 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 | 184 | # --- Setup logging |
174 | 185 | try: |
... | ... | @@ -179,8 +190,9 @@ def main(): |
179 | 190 | sys.exit(1) |
180 | 191 | |
181 | 192 | # --- start application |
193 | + learnapp = LearnApp(arg.conffile[0]) | |
182 | 194 | try: |
183 | - webapp = WebApplication() | |
195 | + webapp = WebApplication(learnapp) | |
184 | 196 | except Exception as e: |
185 | 197 | logging.critical('Can\'t start application.') |
186 | 198 | # sys.exit(1) | ... | ... |
templates/learn.html
... | ... | @@ -66,12 +66,12 @@ |
66 | 66 | <!-- ===================================================================== --> |
67 | 67 | <!-- Container --> |
68 | 68 | <div class="container"> |
69 | -<audio> | |
69 | +<!-- <audio> | |
70 | 70 | <source id="snd-intro" src="/static/sounds/intro.mp3" type="audio/mpeg"> |
71 | 71 | <source id="snd-correct" src="/static/sounds/correct.mp3" type="audio/mpeg"> |
72 | 72 | <source id="snd-wrong" src="/static/sounds/wrong.mp3" type="audio/mpeg"> |
73 | 73 | </audio> |
74 | - | |
74 | + --> | |
75 | 75 | <form action="/question" method="post" id="question_form" autocomplete="off"> |
76 | 76 | {% module xsrf_form_html() %} |
77 | 77 | |
... | ... | @@ -106,9 +106,11 @@ function updateQuestion(response){ |
106 | 106 | MathJax.Hub.Queue(["Typeset",MathJax.Hub,"question_div"]); |
107 | 107 | |
108 | 108 | $("textarea, input:text, input:radio, input:checkbox").keydown(function (e) { |
109 | - if (e.keyCode == 13 && e.shiftKey) { | |
109 | + if (e.keyCode == 13) { | |
110 | 110 | e.preventDefault(); |
111 | - getQuestion(); | |
111 | + if (e.shiftKey) { | |
112 | + getQuestion(); | |
113 | + } | |
112 | 114 | } |
113 | 115 | }); |
114 | 116 | // var audio = new Audio('/static/sounds/correct.mp3'); | ... | ... |
templates/question.html
1 | 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 | 14 | \ No newline at end of file |
15 | +<!-- </div> | |
16 | +</div> | |
17 | + --> | |
16 | 18 | \ No newline at end of file | ... | ... |