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 | ... | ... |