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