Commit bbfe1f31bd7dcc3390e7e388fd522f8632d0ea1e
1 parent
bbc1c506
Exists in
master
and in
1 other branch
- the topic state now has level and date.
Showing
8 changed files
with
49 additions
and
20 deletions
Show diff stats
BUGS.md
| 1 | 1 | |
| 2 | 2 | BUGS: |
| 3 | 3 | |
| 4 | -- guardar state cada vez que topico termina | |
| 4 | +- error if demo.yaml has no topics | |
| 5 | +- pymips a funcionar | |
| 5 | 6 | - reload da página rebenta o estado. |
| 7 | +- guardar state cada vez que topico termina | |
| 6 | 8 | - indicar o topico actual no sidebar |
| 7 | 9 | - session management. close after inactive time. |
| 8 | 10 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() |
| 9 | 11 | |
| 10 | 12 | TODO: |
| 11 | 13 | |
| 12 | -- logs de debug devem indicar o user. | |
| 13 | 14 | - implementar http com redirect para https. |
| 14 | 15 | - topicos no sidebar devem ser links para iniciar um topico acessivel. os inacessiveis devem estar inactivos. |
| 15 | 16 | - usar codemirror no textarea | ... | ... |
app.py
| ... | ... | @@ -3,6 +3,7 @@ |
| 3 | 3 | from contextlib import contextmanager # `with` statement in db sessions |
| 4 | 4 | import logging |
| 5 | 5 | from os import path, sys |
| 6 | +from datetime import datetime | |
| 6 | 7 | |
| 7 | 8 | # user installed libraries |
| 8 | 9 | try: |
| ... | ... | @@ -58,7 +59,12 @@ class LearnApp(object): |
| 58 | 59 | # success |
| 59 | 60 | |
| 60 | 61 | tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid) |
| 61 | - state = {t.topic_id: t.level for t in tt} | |
| 62 | + state = {} | |
| 63 | + for t in tt: | |
| 64 | + state[t.topic_id] = { | |
| 65 | + 'level': t.level, | |
| 66 | + 'date': datetime.strptime(t.date, "%Y-%m-%d %H:%M:%S.%f"), | |
| 67 | + } | |
| 62 | 68 | |
| 63 | 69 | self.online[uid] = { |
| 64 | 70 | 'name': student.name, |
| ... | ... | @@ -79,13 +85,15 @@ class LearnApp(object): |
| 79 | 85 | # update existing associations and remove from state dict |
| 80 | 86 | for a in s.query(StudentTopic).filter_by(student_id=uid): |
| 81 | 87 | if a.topic_id in state: |
| 82 | - a.level = state.pop(a.topic_id) # update | |
| 88 | + d = state.pop(a.topic_id) | |
| 89 | + a.level = d['level'] #state.pop(a.topic_id) # update | |
| 90 | + a.date = str(d['date']) | |
| 83 | 91 | s.add(a) |
| 84 | 92 | |
| 85 | 93 | # insert the remaining ones |
| 86 | 94 | u = s.query(Student).get(uid) |
| 87 | - for n,l in state.items(): | |
| 88 | - a = StudentTopic(level=l) | |
| 95 | + for n,d in state.items(): | |
| 96 | + a = StudentTopic(level=d['level'], date=str(d['date'])) | |
| 89 | 97 | t = s.query(Topic).get(n) |
| 90 | 98 | if t is None: # create if topic doesn't exist yet |
| 91 | 99 | t = Topic(id=n) |
| ... | ... | @@ -134,6 +142,10 @@ class LearnApp(object): |
| 134 | 142 | return path.join(p, topic, 'public') |
| 135 | 143 | |
| 136 | 144 | # ------------------------------------------------------------------------ |
| 145 | + # def get_current_question(self, uid): | |
| 146 | + # return self.online[uid]['state'].current_question | |
| 147 | + | |
| 148 | + # ------------------------------------------------------------------------ | |
| 137 | 149 | # check answer and if correct returns new question, otherise returns None |
| 138 | 150 | def check_answer(self, uid, answer): |
| 139 | 151 | knowledge = self.online[uid]['state'] | ... | ... |
initdb.py
| ... | ... | @@ -10,7 +10,7 @@ import bcrypt |
| 10 | 10 | from sqlalchemy import create_engine |
| 11 | 11 | from sqlalchemy.orm import sessionmaker |
| 12 | 12 | |
| 13 | -from models import Base, Student, Answer | |
| 13 | +from models import Base, Student #, Answer | |
| 14 | 14 | |
| 15 | 15 | # SIIUE names have alien strings like "(TE)" and are sometimes capitalized |
| 16 | 16 | # We remove them so that students dont keep asking what it means | ... | ... |
knowledge.py
| ... | ... | @@ -18,19 +18,21 @@ logger = logging.getLogger(__name__) |
| 18 | 18 | class Knowledge(object): |
| 19 | 19 | def __init__(self, depgraph, state={}): |
| 20 | 20 | self.depgraph = depgraph |
| 21 | - self.state = state # {node: level, node: level, ...} | |
| 22 | - | |
| 23 | 21 | self.topic_sequence = nx.topological_sort(self.depgraph) # FIXME |
| 24 | 22 | |
| 23 | + # state = {'topic_id': {'level':0.5, 'date': datetime}, ...} | |
| 24 | + self.state = state | |
| 25 | + | |
| 25 | 26 | # select a topic to do |
| 26 | 27 | self.new_topic() |
| 27 | 28 | |
| 28 | 29 | # ------------------------------------------------------------------------ |
| 29 | 30 | def new_topic(self, topic=None): |
| 31 | + logger.debug(f'new_topic {topic}') | |
| 30 | 32 | if topic is None: |
| 31 | 33 | # select the first topic that has level < 0.9 |
| 32 | 34 | for topic in self.topic_sequence: |
| 33 | - if self.state.get(topic, 0.0) < 0.9: | |
| 35 | + if topic not in self.state or self.state[topic]['level'] < 0.9: | |
| 34 | 36 | break |
| 35 | 37 | |
| 36 | 38 | # FIXME if all are > 0.9, will stay in the last one forever... |
| ... | ... | @@ -55,17 +57,25 @@ class Knowledge(object): |
| 55 | 57 | |
| 56 | 58 | # ------------------------------------------------------------------------ |
| 57 | 59 | def get_knowledge_state(self): |
| 58 | - return [(t, self.state.get(t, 0.0)) for t in self.topic_sequence] | |
| 60 | + ts = [] | |
| 61 | + for t in self.topic_sequence: | |
| 62 | + if t in self.state: | |
| 63 | + ts.append((t, self.state[t]['level'])) | |
| 64 | + else: | |
| 65 | + ts.append((t, 0.0)) | |
| 66 | + return ts | |
| 67 | + # return [(t, self.state.get(t, 0.0)) for t in self.topic_sequence] | |
| 59 | 68 | |
| 60 | 69 | # ------------------------------------------------------------------------ |
| 61 | 70 | def get_topic_progress(self): |
| 71 | + logger.debug('-> Knowledge.get_topic_progress()') | |
| 62 | 72 | return len(self.finished_questions) / (len(self.finished_questions) + len(self.questions)) |
| 63 | 73 | |
| 64 | 74 | # ------------------------------------------------------------------------ |
| 65 | 75 | # if answer to current question is correct generates a new question |
| 66 | 76 | # otherwise returns none |
| 67 | 77 | def new_question(self): |
| 68 | - logger.debug('Knowledge.new_question()') | |
| 78 | + logger.debug('-> Knowledge.new_question()') | |
| 69 | 79 | |
| 70 | 80 | if self.current_question is None or \ |
| 71 | 81 | self.current_question.get('grade', 0.0) > 0.9: |
| ... | ... | @@ -73,7 +83,10 @@ class Knowledge(object): |
| 73 | 83 | # if no more questions in this topic, go to the next one |
| 74 | 84 | # keep going if there are no questions in the next topics |
| 75 | 85 | while not self.questions: |
| 76 | - self.state[self.current_topic] = 1.0 | |
| 86 | + self.state[self.current_topic] = { | |
| 87 | + 'level': 1.0, | |
| 88 | + 'date': datetime.now() | |
| 89 | + } | |
| 77 | 90 | self.new_topic() |
| 78 | 91 | |
| 79 | 92 | self.current_question = self.questions.pop(0) |
| ... | ... | @@ -82,10 +95,11 @@ class Knowledge(object): |
| 82 | 95 | |
| 83 | 96 | return self.current_question |
| 84 | 97 | |
| 98 | + | |
| 85 | 99 | # --- checks answer ------------------------------------------------------ |
| 86 | 100 | # returns current question with correction, time and comments updated |
| 87 | 101 | def check_answer(self, answer): |
| 88 | - logger.debug(f'Knowledge.check_answer({answer})') | |
| 102 | + logger.debug(f'-> Knowledge.check_answer({answer})') | |
| 89 | 103 | question = self.current_question |
| 90 | 104 | if question is not None: |
| 91 | 105 | question['finish_time'] = datetime.now() | ... | ... |
models.py
| ... | ... | @@ -15,7 +15,7 @@ class StudentTopic(Base): |
| 15 | 15 | student_id = Column(String, ForeignKey('students.id'), primary_key=True) |
| 16 | 16 | topic_id = Column(String, ForeignKey('topics.id'), primary_key=True) |
| 17 | 17 | level = Column(Float) |
| 18 | - # date = Column(String) | |
| 18 | + date = Column(String) | |
| 19 | 19 | |
| 20 | 20 | # --- |
| 21 | 21 | student = relationship('Student', back_populates='topics') | ... | ... |
questions.py
| ... | ... | @@ -434,7 +434,7 @@ class QFactory(object): |
| 434 | 434 | try: |
| 435 | 435 | qinstance = self._types[q['type']](q) # instance with correct class |
| 436 | 436 | except KeyError as e: |
| 437 | - logger.error(f'Unknown question type "{q["type"]}"') | |
| 437 | + logger.error(f'Failed to generate question "{q["ref"]}"') | |
| 438 | 438 | raise e |
| 439 | 439 | else: |
| 440 | 440 | return qinstance | ... | ... |
serve.py
| ... | ... | @@ -157,9 +157,9 @@ class QuestionHandler(BaseHandler): |
| 157 | 157 | 'textarea': 'question-textarea.html', |
| 158 | 158 | } |
| 159 | 159 | |
| 160 | - @tornado.web.authenticated | |
| 161 | - def get(self): | |
| 162 | - self.redirect('/') | |
| 160 | + # @tornado.web.authenticated | |
| 161 | + # def get(self): # FIXME unused | |
| 162 | + # self.redirect('/') | |
| 163 | 163 | |
| 164 | 164 | @tornado.web.authenticated |
| 165 | 165 | def post(self): |
| ... | ... | @@ -222,6 +222,8 @@ def main(): |
| 222 | 222 | print('Common causes:\n - inexistent directory "logs"?\n - write permission to "logs" directory?') |
| 223 | 223 | sys.exit(1) |
| 224 | 224 | |
| 225 | + logging.info('===========================================================') | |
| 226 | + | |
| 225 | 227 | # --- start application |
| 226 | 228 | learnapp = LearnApp(arg.conffile[0]) |
| 227 | 229 | try: | ... | ... |
templates/learn.html
| ... | ... | @@ -153,7 +153,7 @@ $.fn.extend({ |
| 153 | 153 | } |
| 154 | 154 | }); |
| 155 | 155 | |
| 156 | -// Processes the response given by the served after an answer is submitted. | |
| 156 | +// Processes the response given by the server after an answer is submitted. | |
| 157 | 157 | function updateQuestion(response){ |
| 158 | 158 | switch (response["method"]) { |
| 159 | 159 | case "new_question": | ... | ... |