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 |