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