Commit 065611f74a7e84ff9760b271d4f81a9303e8a80b
1 parent
987cf289
Exists in
master
and in
1 other branch
large ammount of changes, main ones:
- allow multiple instances of same question - wrong answer => new instance of question added at the end of topic - show current topic on sidebar - fixed reload crash - fixed text show previous answer if incorrect not yet working.
Showing
9 changed files
with
284 additions
and
247 deletions
Show diff stats
BUGS.md
| 1 | 1 | |
| 2 | 2 | BUGS: |
| 3 | 3 | |
| 4 | -- nao permite perguntas repetidas. iterar questions da configuracao em vez das do ficheiro. ver app.py linha 223. | |
| 5 | -- pymips: activar/desactivar instruções | |
| 4 | +- topicos no sidebar devem ser links para iniciar um topico acessivel. os inacessiveis devem estar inactivos. | |
| 6 | 5 | - tabs em textarea nao funcionam correctamente (insere 1 espaco em vez de 4) |
| 7 | 6 | - reportar comentarios após submeter. |
| 8 | -- logs debug mostrar user | |
| 9 | -- logs mostrar fim de topico | |
| 10 | -- textarea, text devem mostrar no html os valores iniciais de ans, se existir | |
| 7 | +- textarea deve mostrar no html os valores iniciais de ans, se existir | |
| 11 | 8 | - detect questions in questions.yaml without ref -> error ou generate default. |
| 12 | 9 | - error if demo.yaml has no topics |
| 13 | -- reload da página rebenta o estado. | |
| 14 | 10 | - guardar state cada vez que topico termina |
| 15 | -- indicar o topico actual no sidebar | |
| 16 | 11 | - session management. close after inactive time. |
| 17 | 12 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() |
| 18 | 13 | - titulos das perguntas não suportam markdown |
| 19 | 14 | |
| 20 | 15 | TODO: |
| 21 | 16 | |
| 22 | -- implementar http com redirect para https. | |
| 23 | -- topicos no sidebar devem ser links para iniciar um topico acessivel. os inacessiveis devem estar inactivos. | |
| 17 | +- pymips: activar/desactivar instruções | |
| 18 | +- implementar servidor http com redirect para https. | |
| 24 | 19 | - usar codemirror no textarea |
| 25 | -- mostrar comments quando falha a resposta | |
| 26 | 20 | - generators not working: bcrypt (ver blog) |
| 27 | 21 | |
| 28 | 22 | FIXED: |
| 29 | 23 | |
| 24 | +- logs inicio de topico | |
| 25 | +- indicar o topico actual no sidebar | |
| 26 | +- reload da página rebenta o estado. | |
| 27 | +- text deve mostrar no html os valores iniciais de ans, se existir | |
| 28 | +- nao permite perguntas repetidas. iterar questions da configuracao em vez das do ficheiro. ver app.py linha 223. | |
| 30 | 29 | - level depender do numero de respostas correctas |
| 31 | 30 | - pymips a funcionar |
| 32 | 31 | - logs mostram que está a gerar cada pergunta 2 vezes...?? | ... | ... |
app.py
| ... | ... | @@ -37,8 +37,8 @@ class LearnApp(object): |
| 37 | 37 | # online students |
| 38 | 38 | self.online = {} |
| 39 | 39 | |
| 40 | - # build dependency graph | |
| 41 | - self.build_dependency_graph(conffile) | |
| 40 | + self.depgraph = build_dependency_graph(conffile) | |
| 41 | + | |
| 42 | 42 | |
| 43 | 43 | # connect to database and check registered students |
| 44 | 44 | self.db_setup(self.depgraph.graph['database']) |
| ... | ... | @@ -62,6 +62,7 @@ class LearnApp(object): |
| 62 | 62 | return False # wrong password |
| 63 | 63 | |
| 64 | 64 | # success |
| 65 | + logger.info(f'User "{uid}" logged in') | |
| 65 | 66 | |
| 66 | 67 | tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid) |
| 67 | 68 | state = {} |
| ... | ... | @@ -76,7 +77,6 @@ class LearnApp(object): |
| 76 | 77 | 'number': student.id, |
| 77 | 78 | 'state': Knowledge(self.depgraph, state=state, student=student.id) |
| 78 | 79 | } |
| 79 | - logger.info(f'User "{uid}" logged in') | |
| 80 | 80 | return True |
| 81 | 81 | |
| 82 | 82 | # ------------------------------------------------------------------------ |
| ... | ... | @@ -124,115 +124,23 @@ class LearnApp(object): |
| 124 | 124 | return True |
| 125 | 125 | |
| 126 | 126 | # ------------------------------------------------------------------------ |
| 127 | - def get_student_name(self, uid): | |
| 128 | - return self.online[uid].get('name', '') | |
| 129 | - | |
| 130 | - # ------------------------------------------------------------------------ | |
| 131 | - def get_student_state(self, uid): | |
| 132 | - return self.online[uid]['state'].get_knowledge_state() | |
| 133 | - | |
| 134 | - # ------------------------------------------------------------------------ | |
| 135 | - def get_student_progress(self, uid): | |
| 136 | - return self.online[uid]['state'].get_topic_progress() | |
| 137 | - | |
| 138 | - # ------------------------------------------------------------------------ | |
| 139 | - def get_student_question(self, uid): | |
| 140 | - return self.online[uid]['state'].get_current_question() # dict | |
| 141 | - | |
| 142 | - # ------------------------------------------------------------------------ | |
| 143 | - def get_title(self): | |
| 144 | - return self.depgraph.graph['title'] | |
| 145 | - | |
| 146 | - # ------------------------------------------------------------------------ | |
| 147 | - def get_topic_name(self, ref): | |
| 148 | - return self.depgraph.node[ref]['name'] | |
| 149 | - | |
| 150 | - # ------------------------------------------------------------------------ | |
| 151 | - def get_current_public_dir(self, uid): | |
| 152 | - topic = self.online[uid]['state'].get_current_topic() | |
| 153 | - p = self.depgraph.graph['path'] | |
| 154 | - return path.join(p, topic, 'public') | |
| 155 | - | |
| 156 | - # ------------------------------------------------------------------------ | |
| 157 | 127 | # check answer and if correct returns new question, otherwise returns None |
| 158 | 128 | # ------------------------------------------------------------------------ |
| 159 | 129 | def check_answer(self, uid, answer): |
| 160 | 130 | knowledge = self.online[uid]['state'] |
| 161 | - current_question = knowledge.check_answer(answer) | |
| 131 | + q = knowledge.check_answer(answer) | |
| 162 | 132 | |
| 163 | - if current_question is not None: | |
| 164 | - logger.debug('check_answer: saving answer to db ...') | |
| 165 | - with self.db_session() as s: | |
| 166 | - s.add(Answer( | |
| 167 | - ref=current_question['ref'], | |
| 168 | - grade=current_question['grade'], | |
| 169 | - starttime=str(current_question['start_time']), | |
| 170 | - finishtime=str(current_question['finish_time']), | |
| 171 | - student_id=uid)) | |
| 172 | - s.commit() | |
| 173 | - | |
| 174 | - return knowledge.new_question() | |
| 175 | - | |
| 176 | - # ------------------------------------------------------------------------ | |
| 177 | - # Given configuration file, loads YAML on that file and builds the graph. | |
| 178 | - # First, topics such as `computer/mips/exceptions` are added as nodes | |
| 179 | - # together with dependencies. Then, questions are loaded to a factory. | |
| 180 | - # ------------------------------------------------------------------------ | |
| 181 | - def build_dependency_graph(self, config_file): | |
| 182 | - | |
| 183 | - # Load configuration file to a dict | |
| 184 | - try: | |
| 185 | - with open(config_file, 'r') as f: | |
| 186 | - config = yaml.load(f) | |
| 187 | - except FileNotFoundError: | |
| 188 | - logger.critical(f'File not found: "{config_file}"') | |
| 189 | - raise LearnAppException | |
| 190 | - except yaml.scanner.ScannerError as err: | |
| 191 | - logger.critical(f'Parsing YAML file "{config_file}": {err}') | |
| 192 | - raise LearnAppException | |
| 193 | - else: | |
| 194 | - logger.info(f'Configuration file "{config_file}"') | |
| 195 | - | |
| 196 | - # create graph | |
| 197 | - prefix = config.get('path', '.') | |
| 198 | - title = config.get('title', '') | |
| 199 | - database = config.get('database', 'students.db') | |
| 200 | - g = nx.DiGraph(path=prefix, title=title, database=database) | |
| 201 | - | |
| 202 | - # iterate over topics and build graph | |
| 203 | - topics = config.get('topics', {}) | |
| 204 | - for ref,attr in topics.items(): | |
| 205 | - g.add_node(ref) | |
| 206 | - if isinstance(attr, dict): | |
| 207 | - g.node[ref]['name'] = attr.get('name', ref) | |
| 208 | - g.node[ref]['questions'] = attr.get('questions', []) | |
| 209 | - g.add_edges_from((d,ref) for d in attr.get('deps', [])) | |
| 210 | - | |
| 211 | - # iterate over topics and create question factories | |
| 212 | - logger.info('Loading:') | |
| 213 | - for ref in g.nodes_iter(): | |
| 214 | - fullpath = path.expanduser(path.join(prefix, ref)) | |
| 215 | - filename = path.join(fullpath, 'questions.yaml') | |
| 216 | - | |
| 217 | - loaded_questions = load_yaml(filename, default=[]) | |
| 218 | - | |
| 219 | - # make dict from list of questions for easier selection | |
| 220 | - qdict = {q['ref']: q for q in loaded_questions} | |
| 221 | - | |
| 222 | - # 'questions' not provided in configuration means load all | |
| 223 | - if not g.node[ref]['questions']: | |
| 224 | - g.node[ref]['questions'] = qdict.keys() #[q['ref'] for q in loaded_questions] | |
| 225 | - | |
| 226 | - g.node[ref]['factory'] = [] | |
| 227 | - for qref in g.node[ref]['questions']: | |
| 228 | - q = qdict[qref] | |
| 229 | - q['path'] = fullpath | |
| 230 | - g.node[ref]['factory'].append(QFactory(q)) | |
| 133 | + with self.db_session() as s: | |
| 134 | + s.add(Answer( | |
| 135 | + ref=q['ref'], | |
| 136 | + grade=q['grade'], | |
| 137 | + starttime=str(q['start_time']), | |
| 138 | + finishtime=str(q['finish_time']), | |
| 139 | + student_id=uid)) | |
| 140 | + s.commit() | |
| 231 | 141 | |
| 232 | - logger.info(f' {len(g.node[ref]["factory"])} questions from "{ref}"') | |
| 142 | + return q['grade'] | |
| 233 | 143 | |
| 234 | - self.depgraph = g | |
| 235 | - return g | |
| 236 | 144 | |
| 237 | 145 | # ------------------------------------------------------------------------ |
| 238 | 146 | # Fill db table 'Topic' with topics from the graph if not already there. |
| ... | ... | @@ -274,3 +182,113 @@ class LearnApp(object): |
| 274 | 182 | finally: |
| 275 | 183 | session.close() |
| 276 | 184 | |
| 185 | + | |
| 186 | + | |
| 187 | + # ======================================================================== | |
| 188 | + # methods that do not change state (pure functions) | |
| 189 | + # ======================================================================== | |
| 190 | + | |
| 191 | + | |
| 192 | + # ------------------------------------------------------------------------ | |
| 193 | + def get_student_name(self, uid): | |
| 194 | + return self.online[uid].get('name', '') | |
| 195 | + | |
| 196 | + # ------------------------------------------------------------------------ | |
| 197 | + def get_student_state(self, uid): | |
| 198 | + return self.online[uid]['state'].get_knowledge_state() | |
| 199 | + | |
| 200 | + # ------------------------------------------------------------------------ | |
| 201 | + def get_student_progress(self, uid): | |
| 202 | + return self.online[uid]['state'].get_topic_progress() | |
| 203 | + | |
| 204 | + # ------------------------------------------------------------------------ | |
| 205 | + def get_student_question(self, uid): | |
| 206 | + return self.online[uid]['state'].get_current_question() # dict | |
| 207 | + | |
| 208 | + # ------------------------------------------------------------------------ | |
| 209 | + def get_student_topic(self, uid): | |
| 210 | + return self.online[uid]['state'].get_current_topic() # str | |
| 211 | + | |
| 212 | + # ------------------------------------------------------------------------ | |
| 213 | + def get_title(self): | |
| 214 | + return self.depgraph.graph['title'] | |
| 215 | + | |
| 216 | + # ------------------------------------------------------------------------ | |
| 217 | + def get_topic_name(self, ref): | |
| 218 | + return self.depgraph.node[ref]['name'] | |
| 219 | + | |
| 220 | + # ------------------------------------------------------------------------ | |
| 221 | + def get_current_public_dir(self, uid): | |
| 222 | + topic = self.online[uid]['state'].get_current_topic() | |
| 223 | + p = self.depgraph.graph['path'] | |
| 224 | + return path.join(p, topic, 'public') | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | +# ============================================================================ | |
| 229 | +# Given configuration file, loads YAML on that file and builds a digraph. | |
| 230 | +# First, topics such as `computer/mips/exceptions` are added as nodes | |
| 231 | +# together with dependencies. Then, questions are loaded to a factory. | |
| 232 | +# | |
| 233 | +# g.graph['path'] base path where topic directories are located | |
| 234 | +# g.graph['title'] title defined in the configuration YAML | |
| 235 | +# g.graph['database'] sqlite3 database file to use | |
| 236 | +# | |
| 237 | +# Nodes are the topic references e.g. 'my/topic' | |
| 238 | +# g.node['my/topic']['name'] name of the topic | |
| 239 | +# g.node['my/topic']['questions'] list of question refs defined in YAML | |
| 240 | +# g.node['my/topic']['factory'] dict with question factories | |
| 241 | +# ---------------------------------------------------------------------------- | |
| 242 | +def build_dependency_graph(config_file): | |
| 243 | + | |
| 244 | + # Load configuration file to a dict | |
| 245 | + try: | |
| 246 | + with open(config_file, 'r') as f: | |
| 247 | + config = yaml.load(f) | |
| 248 | + except FileNotFoundError: | |
| 249 | + logger.critical(f'File not found: "{config_file}"') | |
| 250 | + raise LearnAppException | |
| 251 | + except yaml.scanner.ScannerError as err: | |
| 252 | + logger.critical(f'Parsing YAML file "{config_file}": {err}') | |
| 253 | + raise LearnAppException | |
| 254 | + else: | |
| 255 | + logger.info(f'Configuration file "{config_file}"') | |
| 256 | + | |
| 257 | + # create graph | |
| 258 | + prefix = config.get('path', '.') | |
| 259 | + title = config.get('title', '') | |
| 260 | + database = config.get('database', 'students.db') | |
| 261 | + g = nx.DiGraph(path=prefix, title=title, database=database) | |
| 262 | + | |
| 263 | + # iterate over topics and build graph | |
| 264 | + topics = config.get('topics', {}) | |
| 265 | + for ref,attr in topics.items(): | |
| 266 | + g.add_node(ref) | |
| 267 | + if isinstance(attr, dict): | |
| 268 | + g.node[ref]['name'] = attr.get('name', ref) | |
| 269 | + g.node[ref]['questions'] = attr.get('questions', []) | |
| 270 | + g.add_edges_from((d,ref) for d in attr.get('deps', [])) | |
| 271 | + | |
| 272 | + # iterate over topics and create question factories | |
| 273 | + logger.info('Loading:') | |
| 274 | + for tref in g.nodes_iter(): | |
| 275 | + tnode = g.node[tref] # current node (topic) | |
| 276 | + fullpath = path.expanduser(path.join(prefix, tref)) | |
| 277 | + filename = path.join(fullpath, 'questions.yaml') | |
| 278 | + | |
| 279 | + loaded_questions = load_yaml(filename, default=[]) | |
| 280 | + | |
| 281 | + # if questions not in configuration then load all, preserve order | |
| 282 | + if not tnode['questions']: | |
| 283 | + tnode['questions'] = [q['ref'] for q in loaded_questions] | |
| 284 | + | |
| 285 | + # make questions factory (without repeting same question) | |
| 286 | + tnode['factory'] = {} | |
| 287 | + for q in loaded_questions: | |
| 288 | + if q['ref'] in tnode['questions']: | |
| 289 | + q['path'] = fullpath | |
| 290 | + tnode['factory'][q['ref']] = QFactory(q) | |
| 291 | + | |
| 292 | + logger.info(f' {len(tnode["questions"])} questions from "{tref}"') | |
| 293 | + | |
| 294 | + return g | ... | ... |
knowledge.py
| ... | ... | @@ -17,41 +17,87 @@ logger = logging.getLogger(__name__) |
| 17 | 17 | # kowledge state of each student....?? |
| 18 | 18 | # ---------------------------------------------------------------------------- |
| 19 | 19 | class Knowledge(object): |
| 20 | + # ======================================================================= | |
| 21 | + # methods that update state | |
| 22 | + # ======================================================================= | |
| 23 | + | |
| 20 | 24 | def __init__(self, depgraph, state={}, student=''): |
| 21 | 25 | self.depgraph = depgraph |
| 22 | 26 | self.state = state # {'topic_id': {'level':0.5, 'date': datetime}, ...} |
| 23 | 27 | self.student = student |
| 24 | 28 | |
| 25 | - # compute recommended sequence of topics (FIXME) | |
| 29 | + # compute recommended sequence of topics ['a', 'b',...] | |
| 26 | 30 | self.topic_sequence = nx.topological_sort(self.depgraph) |
| 27 | 31 | |
| 28 | - # select a topic to do | |
| 29 | - self.new_topic() | |
| 32 | + print(self.topic_sequence) | |
| 33 | + print(self.depgraph.edges()) | |
| 34 | + | |
| 35 | + # select a topic to do and initialize questions | |
| 36 | + self.start_topic() | |
| 30 | 37 | |
| 31 | 38 | # ------------------------------------------------------------------------ |
| 32 | 39 | # Start a new topic. If not provided, selects the first with level < 0.8 |
| 33 | 40 | # If all levels > 0.8, will stay in the last one forever... |
| 34 | 41 | # ------------------------------------------------------------------------ |
| 35 | - def new_topic(self, topic=None): | |
| 42 | + def start_topic(self, topic=None): | |
| 36 | 43 | if topic is None: |
| 37 | 44 | for topic in self.topic_sequence: |
| 38 | - if topic not in self.state or self.state[topic]['level'] < 0.8: | |
| 45 | + unlocked = topic in self.state | |
| 46 | + needs_work = unlocked and self.state[topic]['level'] < 0.8 | |
| 47 | + factory = self.depgraph.node[topic]['factory'] | |
| 48 | + if needs_work and factory: | |
| 39 | 49 | break |
| 40 | - | |
| 41 | - logger.info(f'Student {self.student} new topic "{topic}"') | |
| 50 | + # logger.info(f'{self.student} skipped topic "{topic}"') | |
| 51 | + else: | |
| 52 | + factory = self.depgraph.node[topic]['factory'] | |
| 53 | + # FIXME if factory is empty??? | |
| 42 | 54 | |
| 43 | 55 | self.current_topic = topic |
| 44 | - # self.current_topic_idx = self.topic_sequence.index(topic) | |
| 45 | - self.questions = self.generate_questions_for_topic(topic) | |
| 46 | - self.current_question = None | |
| 56 | + logger.info(f'User "{self.student}" topic set to "{topic}"') | |
| 57 | + | |
| 58 | + # generate question instances for current topic | |
| 59 | + questionlist = self.depgraph.node[topic]['questions'] | |
| 60 | + factory = self.depgraph.node[topic]['factory'] | |
| 61 | + self.questions = [factory[qref].generate() for qref in questionlist] | |
| 62 | + | |
| 63 | + self.current_question = self.questions.pop(0) | |
| 64 | + self.current_question['start_time'] = datetime.now() | |
| 47 | 65 | self.finished_questions = [] |
| 48 | - self.correct_answers = 1 | |
| 49 | - self.wrong_answers = 0 | |
| 50 | 66 | |
| 51 | 67 | # ------------------------------------------------------------------------ |
| 52 | - def generate_questions_for_topic(self, topic): | |
| 53 | - factory = self.depgraph.node[topic]['factory'] | |
| 54 | - return [q.generate() for q in factory] | |
| 68 | + # returns the current question with correction, time and comments updated | |
| 69 | + # ------------------------------------------------------------------------ | |
| 70 | + def check_answer(self, answer): | |
| 71 | + q = self.current_question | |
| 72 | + q['finish_time'] = datetime.now() | |
| 73 | + grade = q.correct(answer) | |
| 74 | + logger.debug(f'User {self.student}: grade = {grade}') | |
| 75 | + | |
| 76 | + # new question if answer is correct | |
| 77 | + if grade > 0.999: | |
| 78 | + self.finished_questions.append(q) | |
| 79 | + try: | |
| 80 | + self.current_question = self.questions.pop(0) # FIXME empty? | |
| 81 | + except IndexError: | |
| 82 | + self.current_question = None | |
| 83 | + self.state[self.current_topic] = { | |
| 84 | + 'level': 1.0, | |
| 85 | + 'date': datetime.now() | |
| 86 | + } | |
| 87 | + else: | |
| 88 | + self.current_question['start_time'] = datetime.now() | |
| 89 | + else: | |
| 90 | + # FIXME debug this | |
| 91 | + factory = self.depgraph.node[self.current_topic]['factory'] | |
| 92 | + self.questions.append(factory[q['ref']].generate()) | |
| 93 | + print([q['ref'] for q in self.questions]) | |
| 94 | + | |
| 95 | + return q | |
| 96 | + | |
| 97 | + | |
| 98 | + # ======================================================================== | |
| 99 | + # pure functions of the state (no side effects) | |
| 100 | + # ======================================================================== | |
| 55 | 101 | |
| 56 | 102 | # ------------------------------------------------------------------------ |
| 57 | 103 | def get_current_question(self): |
| ... | ... | @@ -62,8 +108,8 @@ class Knowledge(object): |
| 62 | 108 | return self.current_topic |
| 63 | 109 | |
| 64 | 110 | # ------------------------------------------------------------------------ |
| 65 | - def get_knowledge_state(self): | |
| 66 | - ts = [] # FIXME why list?? | |
| 111 | + def get_knowledge_state(self): # [('topic', 0.9), ...] | |
| 112 | + ts = [] | |
| 67 | 113 | for t in self.topic_sequence: |
| 68 | 114 | if t in self.state: |
| 69 | 115 | ts.append((t, self.state[t]['level'])) |
| ... | ... | @@ -73,44 +119,5 @@ class Knowledge(object): |
| 73 | 119 | |
| 74 | 120 | # ------------------------------------------------------------------------ |
| 75 | 121 | def get_topic_progress(self): |
| 76 | - return len(self.finished_questions) / (len(self.finished_questions) + len(self.questions)) | |
| 77 | - | |
| 78 | - # ------------------------------------------------------------------------ | |
| 79 | - # if answer to current question is correct generates a new question | |
| 80 | - # otherwise returns None | |
| 81 | - def new_question(self): | |
| 82 | - if self.current_question is None or \ | |
| 83 | - self.current_question.get('grade', 0.0) > 0.9: | |
| 84 | - | |
| 85 | - # if no more questions in this topic, go to the next one | |
| 86 | - # keep going if there are no questions in the next topics | |
| 87 | - while not self.questions: | |
| 88 | - self.state[self.current_topic] = { | |
| 89 | - 'level': self.correct_answers / (self.correct_answers + self.wrong_answers), | |
| 90 | - 'date': datetime.now() | |
| 91 | - } | |
| 92 | - self.new_topic() | |
| 93 | - | |
| 94 | - self.current_question = self.questions.pop(0) | |
| 95 | - self.current_question['start_time'] = datetime.now() | |
| 96 | - self.finished_questions.append(self.current_question) | |
| 97 | - | |
| 98 | - logger.debug(f'Student {self.student}: new_question({self.current_question["ref"]})') | |
| 99 | - return self.current_question | |
| 100 | - | |
| 101 | - # ------------------------------------------------------------------------ | |
| 102 | - # returns the current question with correction, time and comments updated | |
| 103 | - # ------------------------------------------------------------------------ | |
| 104 | - def check_answer(self, answer): | |
| 105 | - question = self.current_question | |
| 106 | - if question is not None: | |
| 107 | - question['finish_time'] = datetime.now() | |
| 108 | - grade = question.correct(answer) | |
| 109 | - if grade > 0.9: | |
| 110 | - self.correct_answers += 1 | |
| 111 | - else: | |
| 112 | - self.wrong_answers +=1 | |
| 113 | - | |
| 114 | - logger.debug(f'Student {self.student}: check_answer({answer}) = {grade}') | |
| 122 | + return len(self.finished_questions) / (1 + len(self.finished_questions) + len(self.questions)) | |
| 115 | 123 | |
| 116 | - return question | ... | ... |
questions.py
| ... | ... | @@ -416,20 +416,11 @@ class QFactory(object): |
| 416 | 416 | # which will print a valid question in yaml format to stdout. This |
| 417 | 417 | # output is then yaml parsed into a dictionary `q`. |
| 418 | 418 | if q['type'] == 'generator': |
| 419 | - logger.debug(f'Running script "{q["script"]}"...') | |
| 419 | + logger.debug(f' \_ Running script "{q["script"]}"...') | |
| 420 | 420 | q.setdefault('arg', '') # optional arguments will be sent to stdin |
| 421 | 421 | script = path.join(q['path'], q['script']) |
| 422 | 422 | out = run_script(script=script, stdin=q['arg']) |
| 423 | 423 | q.update(out) |
| 424 | - # try: | |
| 425 | - # q.update(out) | |
| 426 | - # except: | |
| 427 | - # logger.error(f'Question generator "{q["ref"]}"') | |
| 428 | - # q.update({ | |
| 429 | - # 'type': 'alert', | |
| 430 | - # 'title': 'Erro interno', | |
| 431 | - # 'text': 'Ocorreu um erro a gerar esta pergunta.' | |
| 432 | - # }) | |
| 433 | 424 | |
| 434 | 425 | # Finally we create an instance of Question() |
| 435 | 426 | try: | ... | ... |
serve.py
| ... | ... | @@ -160,70 +160,68 @@ class QuestionHandler(BaseHandler): |
| 160 | 160 | # 'alert': '', FIXME |
| 161 | 161 | } |
| 162 | 162 | |
| 163 | - @tornado.web.authenticated | |
| 164 | - def get(self): | |
| 165 | - user = self.current_user | |
| 166 | - state = self.learn.get_student_state(user) # all topics | |
| 163 | + def new_question(self, user): | |
| 164 | + state = self.learn.get_student_state(user) # all topics [('a', 0.1), ...] | |
| 165 | + current_topic = self.learn.get_student_topic(user) # str | |
| 167 | 166 | progress = self.learn.get_student_progress(user) # float |
| 168 | - question = self.learn.get_current_question(user) | |
| 169 | - print(question) # FIXME cant get current question | |
| 167 | + question = self.learn.get_student_question(user) # dict? | |
| 168 | + | |
| 169 | + question_html = self.render_string(self.templates[question['type']],question=question, md=md) | |
| 170 | + topics_html = self.render_string('topics.html', state=state, current_topic=current_topic, gettopicname=self.learn.get_topic_name) | |
| 171 | + | |
| 172 | + return { | |
| 173 | + 'method': 'new_question', | |
| 174 | + 'params': { | |
| 175 | + 'question': tornado.escape.to_unicode(question_html), | |
| 176 | + 'state': tornado.escape.to_unicode(topics_html), | |
| 177 | + 'progress': progress, | |
| 178 | + } | |
| 179 | + } | |
| 180 | + | |
| 181 | + def shake(self, user): | |
| 182 | + progress = self.learn.get_student_progress(user) # in the current topic | |
| 183 | + return { | |
| 184 | + 'method': 'shake', | |
| 185 | + 'params': { | |
| 186 | + 'progress': progress, | |
| 187 | + } | |
| 188 | + } | |
| 189 | + | |
| 190 | + def finished_topic(self, user): | |
| 191 | + state = self.learn.get_student_state(user) # all topics | |
| 192 | + current_topic = self.learn.get_student_topic(uid) | |
| 170 | 193 | |
| 171 | - question_html = self.render_string( | |
| 172 | - self.templates[question['type']], | |
| 173 | - question=question, # dictionary with the question | |
| 174 | - md=md, # function that renders markdown to html | |
| 175 | - ) | |
| 176 | 194 | topics_html = self.render_string('topics.html', |
| 177 | 195 | state=state, |
| 196 | + current_topic=current_topic, | |
| 178 | 197 | topicname=self.learn.get_topic_name, # translate ref to names |
| 179 | 198 | ) |
| 180 | - print(topics_html) | |
| 181 | - print(question_html) | |
| 182 | - self.write({ | |
| 183 | - 'method': 'new_question', | |
| 199 | + return { | |
| 200 | + 'method': 'finished_topic', | |
| 184 | 201 | 'params': { |
| 185 | - 'question': tornado.escape.to_unicode(question_html), | |
| 186 | 202 | 'state': tornado.escape.to_unicode(topics_html), |
| 187 | - 'progress': progress, | |
| 188 | 203 | } |
| 189 | - }) | |
| 204 | + } | |
| 190 | 205 | |
| 191 | - # posting can change state and return new question | |
| 206 | + @tornado.web.authenticated | |
| 207 | + def get(self): | |
| 208 | + self.write(self.new_question(self.current_user)) | |
| 209 | + | |
| 210 | + # handles answer posted | |
| 192 | 211 | @tornado.web.authenticated |
| 193 | 212 | def post(self): |
| 194 | 213 | user = self.current_user |
| 195 | 214 | answer = self.get_body_arguments('answer') |
| 196 | - next_question = self.learn.check_answer(user, answer) | |
| 197 | - state = self.learn.get_student_state(user) # all topics | |
| 198 | - progress = self.learn.get_student_progress(user) # in the current topic | |
| 199 | - | |
| 200 | - if next_question is not None: | |
| 201 | - question_html = self.render_string( | |
| 202 | - self.templates[next_question['type']], | |
| 203 | - question=next_question, # dictionary with the question | |
| 204 | - md=md, # function that renders markdown to html | |
| 205 | - ) | |
| 206 | - topics_html = self.render_string( | |
| 207 | - 'topics.html', | |
| 208 | - state=state, | |
| 209 | - topicname=self.learn.get_topic_name, # function that translates topic references to names | |
| 210 | - ) | |
| 211 | - | |
| 212 | - self.write({ | |
| 213 | - 'method': 'new_question', | |
| 214 | - 'params': { | |
| 215 | - 'question': tornado.escape.to_unicode(question_html), | |
| 216 | - 'state': tornado.escape.to_unicode(topics_html), | |
| 217 | - 'progress': progress, | |
| 218 | - }, | |
| 219 | - }) | |
| 215 | + grade = self.learn.check_answer(user, answer) | |
| 216 | + question = self.learn.get_student_question(user) # same, new or None | |
| 217 | + | |
| 218 | + if question is None: | |
| 219 | + self.write(self.finished_topic(user)) | |
| 220 | + elif grade > 0.999: | |
| 221 | + self.write(self.new_question(user)) | |
| 220 | 222 | else: |
| 221 | - self.write({ | |
| 222 | - 'method': 'shake', | |
| 223 | - 'params': { | |
| 224 | - 'progress': progress, | |
| 225 | - }, | |
| 226 | - }) | |
| 223 | + self.write(self.shake(user)) | |
| 224 | + | |
| 227 | 225 | |
| 228 | 226 | # ---------------------------------------------------------------------------- |
| 229 | 227 | def main(): |
| ... | ... | @@ -262,15 +260,15 @@ def main(): |
| 262 | 260 | }) |
| 263 | 261 | http_server.listen(8443) |
| 264 | 262 | |
| 265 | - # --- start webserver | |
| 263 | + # --- run webserver | |
| 266 | 264 | logging.info('Webserver running...') |
| 265 | + | |
| 267 | 266 | try: |
| 268 | - tornado.ioloop.IOLoop.current().start() | |
| 269 | - # running... | |
| 267 | + tornado.ioloop.IOLoop.current().start() # running... | |
| 270 | 268 | except KeyboardInterrupt: |
| 271 | 269 | tornado.ioloop.IOLoop.current().stop() |
| 272 | - finally: | |
| 273 | - logging.critical('Webserver stopped.') | |
| 270 | + | |
| 271 | + logging.critical('Webserver stopped.') | |
| 274 | 272 | |
| 275 | 273 | # ---------------------------------------------------------------------------- |
| 276 | 274 | if __name__ == "__main__": | ... | ... |
templates/learn.html
| ... | ... | @@ -153,7 +153,7 @@ $.fn.extend({ |
| 153 | 153 | } |
| 154 | 154 | }); |
| 155 | 155 | |
| 156 | -// Processes the response given by the server after an answer is submitted. | |
| 156 | +// Process the response given by the server | |
| 157 | 157 | function updateQuestion(response){ |
| 158 | 158 | switch (response["method"]) { |
| 159 | 159 | case "new_question": |
| ... | ... | @@ -194,12 +194,20 @@ function updateQuestion(response){ |
| 194 | 194 | // audio.play(); |
| 195 | 195 | $('#question_div').animateCSS('zoomIn'); |
| 196 | 196 | break; |
| 197 | + | |
| 197 | 198 | case "shake": |
| 198 | 199 | // var audio = new Audio('/static/sounds/wrong.mp3'); |
| 199 | 200 | // audio.play(); |
| 200 | 201 | $('#topic_progress').css('width', (100*response["params"]["progress"])+'%').attr('aria-valuenow', 100*response["params"]["progress"]); |
| 201 | 202 | $('#question_div').animateCSS('shake'); |
| 202 | 203 | break; |
| 204 | + | |
| 205 | + case "finished_topic": | |
| 206 | + $('#topic_progress').css('width', '100%').attr('aria-valuenow', 100); | |
| 207 | + $("#topics").html(response["params"]["state"]); | |
| 208 | + | |
| 209 | + $("#question_div").html('<img src="/static/trophy.png" alt="trophy" class="img-rounded img-responsive center-block">'); // FIXME size | |
| 210 | + break; | |
| 203 | 211 | } |
| 204 | 212 | } |
| 205 | 213 | |
| ... | ... | @@ -217,14 +225,12 @@ function postQuestion() { |
| 217 | 225 | }); |
| 218 | 226 | } |
| 219 | 227 | |
| 220 | -// Send answer and receive a response. | |
| 221 | -// The response can be a new_question or a shake if the answer is wrong. | |
| 228 | +// Get current question | |
| 222 | 229 | function getQuestion() { |
| 223 | 230 | $.ajax({ |
| 224 | 231 | // type: "GET", |
| 225 | 232 | url: "/question", |
| 226 | 233 | // headers: {"X-XSRFToken": token}, |
| 227 | - // data: $("#question_form").serialize(), // {'a':10,'b':20}, | |
| 228 | 234 | dataType: "json", // expected from server |
| 229 | 235 | success: updateQuestion, |
| 230 | 236 | error: function() {alert("O servidor não responde.");} | ... | ... |
templates/question-text.html
| ... | ... | @@ -2,7 +2,11 @@ |
| 2 | 2 | |
| 3 | 3 | {% block answer %} |
| 4 | 4 | <fieldset data-role="controlgroup"> |
| 5 | - <input type="text" class="form-control" id="answer" name="answer" value="{{ question['answer'] or '' }}" autofocus> | |
| 5 | + {% if question['answer'] %} | |
| 6 | + <input type="text" class="form-control" id="answer" name="answer" value="{{ question['answer'][0] }}" autofocus> | |
| 7 | + {% else %} | |
| 8 | + <input type="text" class="form-control" id="answer" name="answer" value="" autofocus> | |
| 9 | + {% end %} | |
| 6 | 10 | </fieldset><br /> |
| 7 | 11 | <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> |
| 8 | 12 | {% end %} | ... | ... |
templates/question-textarea.html
| 1 | 1 | {% extends "question.html" %} |
| 2 | 2 | |
| 3 | 3 | {% block answer %} |
| 4 | + | |
| 4 | 5 | <textarea class="form-control" rows="{{ question['lines'] }}" name="answer" autofocus>{{ question['answer'] or '' }}</textarea><br /> |
| 5 | 6 | <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> |
| 7 | + | |
| 6 | 8 | {% end %} | ... | ... |
templates/topics.html
| ... | ... | @@ -4,10 +4,22 @@ |
| 4 | 4 | |
| 5 | 5 | <ul class="nav nav-pills nav-stacked"> |
| 6 | 6 | {% for t in state %} |
| 7 | - <li role="presentation" class="disabled"> <!-- class="active" --> | |
| 8 | - <a href="#" class="disabled">{{ topicname(t[0]) }}<br> | |
| 9 | - {{ round(t[1]*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t[1]*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }} | |
| 10 | - </a> | |
| 11 | - </li> | |
| 7 | + | |
| 8 | + {% if t[0] == current_topic %} | |
| 9 | + <li class="active"> <!-- class="active" class="disabled" --> | |
| 10 | + | |
| 11 | + <a> {{ gettopicname(t[0]) }}<br> | |
| 12 | + {{ round(t[1]*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t[1]*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }} | |
| 13 | + </a> | |
| 14 | + | |
| 15 | + {% else %} | |
| 16 | + <li> <!-- class="active" class="disabled" --> | |
| 17 | + | |
| 18 | + <a href="#"> {{ gettopicname(t[0]) }}<br> | |
| 19 | + {{ round(t[1]*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t[1]*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }} | |
| 20 | + </a> | |
| 21 | + | |
| 22 | + {% end %} | |
| 23 | + </li> | |
| 12 | 24 | {% end %} |
| 13 | -</ul> | |
| 14 | 25 | \ No newline at end of file |
| 26 | +</ul> | ... | ... |