Commit ed34db4c3f94e62db9d0a52efa31a21acee63133
1 parent
df6c6afa
Exists in
master
and in
1 other branch
- save topic state on logout
Showing
6 changed files
with
48 additions
and
143 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... | ||
| 3 | - load/save the knowledge state of the student | 4 | - load/save the knowledge state of the student |
| 4 | - se students.db não existe, rebenta. | 5 | - se students.db não existe, rebenta. |
| 5 | - database hardcoded in LearnApp. | 6 | - database hardcoded in LearnApp. |
app.py
| @@ -66,8 +66,31 @@ class LearnApp(object): | @@ -66,8 +66,31 @@ class LearnApp(object): | ||
| 66 | # ------------------------------------------------------------------------ | 66 | # ------------------------------------------------------------------------ |
| 67 | # logout | 67 | # logout |
| 68 | def logout(self, uid): | 68 | def logout(self, uid): |
| 69 | + state = self.online[uid]['state'].state # dict {node:level,...} | ||
| 70 | + print(state) | ||
| 71 | + | ||
| 72 | + with self.db_session(autoflush=False) as s: | ||
| 73 | + | ||
| 74 | + # update existing associations and remove from state dict | ||
| 75 | + aa = s.query(StudentTopic).filter_by(student_id=uid).all() | ||
| 76 | + for a in aa: | ||
| 77 | + print('update ', a) | ||
| 78 | + a.level = state.pop(a.topic_id) # update | ||
| 79 | + s.add_all(aa) | ||
| 80 | + | ||
| 81 | + # insert the remaining ones | ||
| 82 | + u = s.query(Student).get(uid) | ||
| 83 | + for n,l in state.items(): | ||
| 84 | + a = StudentTopic(level=l) | ||
| 85 | + t = s.query(Topic).get(n) | ||
| 86 | + if t is None: # create if topic doesn't exist yet | ||
| 87 | + t = Topic(n) | ||
| 88 | + a.topic = t | ||
| 89 | + u.topics.append(a) | ||
| 90 | + s.add(a) | ||
| 91 | + | ||
| 92 | + del self.online[uid] | ||
| 69 | logger.info(f'User "{uid}" logged out') | 93 | logger.info(f'User "{uid}" logged out') |
| 70 | - del self.online[uid] # FIXME save current state | ||
| 71 | 94 | ||
| 72 | # ------------------------------------------------------------------------ | 95 | # ------------------------------------------------------------------------ |
| 73 | def get_student_name(self, uid): | 96 | def get_student_name(self, uid): |
| @@ -105,8 +128,8 @@ class LearnApp(object): | @@ -105,8 +128,8 @@ class LearnApp(object): | ||
| 105 | # helper to manage db sessions using the `with` statement, for example | 128 | # helper to manage db sessions using the `with` statement, for example |
| 106 | # with self.db_session() as s: s.query(...) | 129 | # with self.db_session() as s: s.query(...) |
| 107 | @contextmanager | 130 | @contextmanager |
| 108 | - def db_session(self): | ||
| 109 | - session = self.Session() | 131 | + def db_session(self, **kw): |
| 132 | + session = self.Session(**kw) | ||
| 110 | try: | 133 | try: |
| 111 | yield session | 134 | yield session |
| 112 | session.commit() | 135 | session.commit() |
knowledge.py
| @@ -24,6 +24,8 @@ class Knowledge(object): | @@ -24,6 +24,8 @@ class Knowledge(object): | ||
| 24 | self.seq = nx.topological_sort(self.depgraph) | 24 | self.seq = nx.topological_sort(self.depgraph) |
| 25 | self.topic = None | 25 | self.topic = None |
| 26 | 26 | ||
| 27 | + self.state = {'a': 3, 'b': 4} | ||
| 28 | + | ||
| 27 | def get_current_question(self): | 29 | def get_current_question(self): |
| 28 | return self.current_question | 30 | return self.current_question |
| 29 | 31 |
models.py
questions.py
| 1 | - | ||
| 2 | -# We start with an empty QuestionFactory() that will be populated with | ||
| 3 | -# question generators that we can load from YAML files. | ||
| 4 | -# To generate an instance of a question we use the method generate(ref) where | 1 | +# QFactory is a class that can generate question instances, e.g. by shuffling |
| 2 | +# options, running a script to generate the question, etc. | ||
| 3 | +# | ||
| 4 | +# To generate an instance of a question we use the method generate() where | ||
| 5 | # the argument is the reference of the question we wish to produce. | 5 | # the argument is the reference of the question we wish to produce. |
| 6 | # | 6 | # |
| 7 | # Example: | 7 | # Example: |
| 8 | # | 8 | # |
| 9 | -# # read everything from question files | ||
| 10 | -# factory = QuestionFactory() | ||
| 11 | -# factory.load_files(['file1.yaml', 'file1.yaml'], '/path/to') | ||
| 12 | -# | ||
| 13 | -# question = factory.generate('some_ref') | 9 | +# # read question from file |
| 10 | +# qdict = tools.load_yaml(filename) | ||
| 11 | +# qfactory = QFactory(question) | ||
| 12 | +# question = qfactory.generate() | ||
| 14 | # | 13 | # |
| 15 | # # experiment answering one question and correct it | 14 | # # experiment answering one question and correct it |
| 16 | -# question['answer'] = 42 # insert answer | 15 | +# question.updateAnswer('42') # insert answer |
| 17 | # grade = question.correct() # correct answer | 16 | # grade = question.correct() # correct answer |
| 18 | 17 | ||
| 19 | # An instance of an actual question is an object that inherits from Question() | 18 | # An instance of an actual question is an object that inherits from Question() |
| @@ -28,7 +27,6 @@ | @@ -28,7 +27,6 @@ | ||
| 28 | 27 | ||
| 29 | import random | 28 | import random |
| 30 | import re | 29 | import re |
| 31 | -# import subprocess | ||
| 32 | from os import path | 30 | from os import path |
| 33 | import logging | 31 | import logging |
| 34 | import sys | 32 | import sys |
| @@ -419,8 +417,8 @@ class QFactory(object): | @@ -419,8 +417,8 @@ class QFactory(object): | ||
| 419 | if q['type'] == 'generator': | 417 | if q['type'] == 'generator': |
| 420 | logger.debug('Running script to generate question "{0}".'.format(q['ref'])) | 418 | logger.debug('Running script to generate question "{0}".'.format(q['ref'])) |
| 421 | q.setdefault('arg', '') # optional arguments will be sent to stdin | 419 | q.setdefault('arg', '') # optional arguments will be sent to stdin |
| 422 | - print(q['path']) | ||
| 423 | - print(q['script']) | 420 | + # print(q['path']) |
| 421 | + # print(q['script']) | ||
| 424 | script = path.join(q['path'], q['script']) | 422 | script = path.join(q['path'], q['script']) |
| 425 | out = run_script(script=script, stdin=q['arg']) | 423 | out = run_script(script=script, stdin=q['arg']) |
| 426 | q.update(out) | 424 | q.update(out) |
| @@ -450,122 +448,3 @@ class QFactory(object): | @@ -450,122 +448,3 @@ class QFactory(object): | ||
| 450 | logger.debug('returning') | 448 | logger.debug('returning') |
| 451 | return qinstance | 449 | return qinstance |
| 452 | 450 | ||
| 453 | - | ||
| 454 | -# =========================================================================== | ||
| 455 | -# This class contains a pool of questions generators from which particular | ||
| 456 | -# Question() instances are generated using QuestionsFactory.generate(ref). | ||
| 457 | -# =========================================================================== | ||
| 458 | -# class QuestionFactory(dict): | ||
| 459 | -# # Depending on the type of question, a different question class will be | ||
| 460 | -# # instantiated. All these classes derive from the base class `Question`. | ||
| 461 | -# _types = { | ||
| 462 | -# 'radio' : QuestionRadio, | ||
| 463 | -# 'checkbox' : QuestionCheckbox, | ||
| 464 | -# 'text' : QuestionText, | ||
| 465 | -# 'text_regex': QuestionTextRegex, | ||
| 466 | -# 'text_numeric': QuestionTextNumeric, | ||
| 467 | -# 'textarea' : QuestionTextArea, | ||
| 468 | -# # informative panels | ||
| 469 | -# 'information': QuestionInformation, | ||
| 470 | -# 'warning' : QuestionInformation, | ||
| 471 | -# 'alert' : QuestionInformation, | ||
| 472 | -# } | ||
| 473 | - | ||
| 474 | -# # ----------------------------------------------------------------------- | ||
| 475 | -# def __init__(self, questions=None): | ||
| 476 | -# super().__init__() | ||
| 477 | -# if isinstance(questions, dict): | ||
| 478 | -# self.add(questions) | ||
| 479 | -# elif isinstance(questions, str): | ||
| 480 | -# self.load_file(questions) | ||
| 481 | - | ||
| 482 | -# # ----------------------------------------------------------------------- | ||
| 483 | -# # Add single question provided in a dictionary. | ||
| 484 | -# # After this, each question will have at least 'ref' and 'type' keys. | ||
| 485 | -# # ----------------------------------------------------------------------- | ||
| 486 | -# def add(self, question): | ||
| 487 | -# # if ref missing try ref='/path/file.yaml:3' | ||
| 488 | -# try: | ||
| 489 | -# question.setdefault('ref', question['filename'] + ':' + str(question['index'])) | ||
| 490 | -# except KeyError: | ||
| 491 | -# logger.error('Missing "ref". Cannot add question to the pool.') | ||
| 492 | -# return | ||
| 493 | - | ||
| 494 | -# # check duplicate references | ||
| 495 | -# if question['ref'] in self: | ||
| 496 | -# logger.error('Duplicate reference "{0}". Replacing the original one!'.format(question['ref'])) | ||
| 497 | - | ||
| 498 | -# question.setdefault('type', 'information') | ||
| 499 | - | ||
| 500 | -# self[question['ref']] = question | ||
| 501 | -# logger.debug('Added question "{0}" to the pool.'.format(question['ref'])) | ||
| 502 | - | ||
| 503 | -# # ----------------------------------------------------------------------- | ||
| 504 | -# # load single YAML questions file | ||
| 505 | -# # ----------------------------------------------------------------------- | ||
| 506 | -# def load_file(self, filename, questions_dir=''): | ||
| 507 | -# f = path.normpath(path.join(questions_dir, filename)) | ||
| 508 | -# questions = load_yaml(f, default=[]) | ||
| 509 | - | ||
| 510 | -# n = 0 | ||
| 511 | -# for i, q in enumerate(questions): | ||
| 512 | -# if isinstance(q, dict): | ||
| 513 | -# q.update({ | ||
| 514 | -# 'filename': filename, | ||
| 515 | -# 'path': questions_dir, | ||
| 516 | -# 'index': i # position in the file, 0 based | ||
| 517 | -# }) | ||
| 518 | -# self.add(q) # add question | ||
| 519 | -# n += 1 # counter | ||
| 520 | -# else: | ||
| 521 | -# logger.error('Question index {0} from file {1} is not a dictionary. Skipped!'.format(i, filename)) | ||
| 522 | - | ||
| 523 | -# logger.info('Loaded {0} questions from "{1}".'.format(n, filename)) | ||
| 524 | - | ||
| 525 | -# # ----------------------------------------------------------------------- | ||
| 526 | -# # load multiple YAML question files | ||
| 527 | -# # ----------------------------------------------------------------------- | ||
| 528 | -# def load_files(self, files, questions_dir=''): | ||
| 529 | -# for filename in files: | ||
| 530 | -# self.load_file(filename, questions_dir) | ||
| 531 | - | ||
| 532 | -# # ----------------------------------------------------------------------- | ||
| 533 | -# # Given a ref returns an instance of a descendent of Question(), | ||
| 534 | -# # i.e. a question object (radio, checkbox, ...). | ||
| 535 | -# # ----------------------------------------------------------------------- | ||
| 536 | -# def generate(self, ref): | ||
| 537 | - | ||
| 538 | -# # Shallow copy so that script generated questions will not replace | ||
| 539 | -# # the original generators | ||
| 540 | -# q = self[ref].copy() | ||
| 541 | - | ||
| 542 | -# # If question is of generator type, an external program will be run | ||
| 543 | -# # which will print a valid question in yaml format to stdout. This | ||
| 544 | -# # output is then converted to a dictionary and `q` becomes that dict. | ||
| 545 | -# if q['type'] == 'generator': | ||
| 546 | -# logger.debug('Running script to generate question "{0}".'.format(q['ref'])) | ||
| 547 | -# q.setdefault('arg', '') # optional arguments will be sent to stdin | ||
| 548 | -# script = path.normpath(path.join(q['path'], q['script'])) | ||
| 549 | -# out = run_script(script=script, stdin=q['arg']) | ||
| 550 | -# try: | ||
| 551 | -# q.update(out) | ||
| 552 | -# except: | ||
| 553 | -# q.update({ | ||
| 554 | -# 'type': 'alert', | ||
| 555 | -# 'title': 'Erro interno', | ||
| 556 | -# 'text': 'Ocorreu um erro a gerar esta pergunta.' | ||
| 557 | -# }) | ||
| 558 | -# # The generator was replaced by a question but not yet instantiated | ||
| 559 | - | ||
| 560 | -# # Finally we create an instance of Question() | ||
| 561 | -# try: | ||
| 562 | -# qinstance = self._types[q['type']](q) # instance with correct class | ||
| 563 | -# except KeyError as e: | ||
| 564 | -# logger.error('Unknown question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) | ||
| 565 | -# raise e | ||
| 566 | -# except: | ||
| 567 | -# logger.error('Failed to create question "{0}" from file "{1}".'.format(q['ref'], q['filename'])) | ||
| 568 | -# else: | ||
| 569 | -# logger.debug('Generated question "{}".'.format(ref)) | ||
| 570 | -# return qinstance | ||
| 571 | - |
templates/learn.html
| @@ -111,13 +111,13 @@ function updateQuestion(response){ | @@ -111,13 +111,13 @@ function updateQuestion(response){ | ||
| 111 | getQuestion(); | 111 | getQuestion(); |
| 112 | } | 112 | } |
| 113 | }); | 113 | }); |
| 114 | - var audio = new Audio('/static/sounds/correct.mp3'); | ||
| 115 | - audio.play(); | 114 | + // var audio = new Audio('/static/sounds/correct.mp3'); |
| 115 | + // audio.play(); | ||
| 116 | $('#question_div').animateCSS('pulse'); | 116 | $('#question_div').animateCSS('pulse'); |
| 117 | break; | 117 | break; |
| 118 | case "shake": | 118 | case "shake": |
| 119 | - var audio = new Audio('/static/sounds/wrong.mp3'); | ||
| 120 | - audio.play(); | 119 | + // var audio = new Audio('/static/sounds/wrong.mp3'); |
| 120 | + // audio.play(); | ||
| 121 | $('#question_div').animateCSS('shake'); | 121 | $('#question_div').animateCSS('shake'); |
| 122 | break; | 122 | break; |
| 123 | } | 123 | } |
| @@ -138,8 +138,8 @@ function getQuestion() { | @@ -138,8 +138,8 @@ function getQuestion() { | ||
| 138 | } | 138 | } |
| 139 | 139 | ||
| 140 | $(document).ready(function() { | 140 | $(document).ready(function() { |
| 141 | - var audio = new Audio('/static/sounds/intro.mp3'); | ||
| 142 | - audio.play(); | 141 | + // var audio = new Audio('/static/sounds/intro.mp3'); |
| 142 | + // audio.play(); | ||
| 143 | $("#submit").click(getQuestion); | 143 | $("#submit").click(getQuestion); |
| 144 | }); | 144 | }); |
| 145 | </script> | 145 | </script> |