Commit bbfe1f31bd7dcc3390e7e388fd522f8632d0ea1e

Authored by Miguel Barão
1 parent bbc1c506
Exists in master and in 1 other branch dev

- the topic state now has level and date.

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