# python standard library import random from datetime import datetime import logging # libraries import networkx as nx # this project # import questions # setup logger for this module logger = logging.getLogger(__name__) # ---------------------------------------------------------------------------- # kowledge state of each student....?? # ---------------------------------------------------------------------------- class StudentKnowledge(object): # ======================================================================= # methods that update state # ======================================================================= def __init__(self, deps, state={}): # graph with topic dependencies shared between all students self.deps = deps # state only contains information on unlocked topics # {'topic_id': {'level':0.5, 'date': datetime}, ...} self.state = state # update state level based on the elapsed time (no dependencies... FIXME) now = datetime.now() for s in state.values(): dt = now - s['date'] s['level'] *= 0.975 ** dt.days # compute recommended sequence of topics ['a', 'b', ...] self.topic_sequence = list(nx.topological_sort(self.deps)) self.unlock_topics() # ------------------------------------------------------------------------ # Unlock topics whose dependencies are satisfied (> min_level) # ------------------------------------------------------------------------ def unlock_topics(self): min_level = 0.01 # minimum level to unlock for topic in self.topic_sequence: if topic not in self.state: # if locked pred = self.deps.predecessors(topic) if all(d in self.state and self.state[d]['level'] > min_level for d in pred): # dependencies done self.state[topic] = { 'level': 0.0, 'date': datetime.now() } logger.debug(f'unlocked {topic}') # ------------------------------------------------------------------------ # Recommends a topic to practice/learn from the state. # FIXME untested # ------------------------------------------------------------------------ def recommended_topic(self): return min(self.state.items(), key=lambda x: x[1]['level'])[0] # if not topic: # for topic in self.topic_sequence: # unlocked = topic in self.state # needs_work = unlocked and self.state[topic]['level'] < 0.8 # factory = self.deps.node[topic]['factory'] # if factory and (not unlocked or needs_work): # break # use given topic if possible # else: # unlocked = topic in self.state # factory = self.deps.node[topic]['factory'] # if not factory or not unlocked: # logger.debug(f'Can\'t start topic "{topic}".') # return # ------------------------------------------------------------------------ # Start a new topic. If not provided, gets a recommendation. # questions: list of generated questions to do in the topic # finished_questions: [] will contain correctly answered questions # current_question: the current question to be presented # ------------------------------------------------------------------------ def init_topic(self, topic=''): logger.debug(f'StudentKnowledge.init_topic({topic})') if not topic: topic = self.recommended_topic() self.current_topic = topic logger.info(f'Topic set to "{topic}"') # generate question instances for current topic factory = self.deps.node[topic]['factory'] questionlist = self.deps.node[topic]['questions'] self.questions = [factory[qref].generate() for qref in questionlist] self.finished_questions = [] self.current_question = self.questions.pop(0) # FIXME crash if empty self.current_question['start_time'] = datetime.now() # ------------------------------------------------------------------------ # returns the current question with correction, time and comments updated # ------------------------------------------------------------------------ def check_answer(self, answer): logger.debug('StudentKnowledge.check_answer()') q = self.current_question # answers are returned from tornado in a list if q['type'] in ('success', 'information', 'info'): # FIXME danger... q['answer'] = None elif q['type'] != 'checkbox': q['answer'] = answer[0] else: q['answer'] = answer q['finish_time'] = datetime.now() grade = q.correct() logger.debug(f'Grade = {grade:.2} ({q["ref"]})') # if answer is correct, get next question if grade > 0.999: self.finished_questions.append(q) try: self.current_question = self.questions.pop(0) # FIXME empty? except IndexError: # finished topic, no more questions self.current_question = None self.state[self.current_topic] = { 'level': 1.0, 'date': datetime.now() } self.unlock_topics() else: self.current_question['start_time'] = datetime.now() # if answer is wrong, keep same question and add a similar one at the end else: factory = self.deps.node[self.current_topic]['factory'] self.questions.append(factory[q['ref']].generate()) # returns answered and corrected question return q # ======================================================================== # pure functions of the state (no side effects) # ======================================================================== # ------------------------------------------------------------------------ def get_current_question(self): return self.current_question # ------------------------------------------------------------------------ def get_current_topic(self): return self.current_topic # ------------------------------------------------------------------------ # Return list of {ref: 'xpto', name: 'long name', leve: 0.5} # Levels are in the interval [0, 1] if unlocked or None if locked. # Topics unlocked but not yet done have level 0.0. # ------------------------------------------------------------------------ def get_knowledge_state(self): return [{ 'ref': ref, 'name': self.deps.nodes[ref]['name'], 'level': self.state[ref]['level'] if ref in self.state else None } for ref in self.topic_sequence ] # ------------------------------------------------------------------------ def get_topic_progress(self): return len(self.finished_questions) / (1 + len(self.finished_questions) + len(self.questions))