diff --git a/.gitignore b/.gitignore index 3807d39..46f1ed9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ /aprendizations/__pycache__/ /demo/students.db /node_modules/ -/.mypy_cache/ \ No newline at end of file +/.mypy_cache/ +.DS_Store +demo/.DS_Store +demo/solar_system/.DS_Store diff --git a/BUGS.md b/BUGS.md index f730e75..0d03bdc 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,10 +1,11 @@ # BUGS -- não suporta definition lists em markdown +- obter rankings por curso GET course=course_id +- indicar qtos topicos faltam (>=50%) para terminar o curso. +- permite definir goal, mas nao verifica se esta no grafo. rebenta no start_topic. - double click submits twice. - nao esta a seguir o max_tries definido no ficheiro de dependencias. -- obter rankings por curso GET course=course_id - impedir que quando students.db não é encontrado, crie um ficheiro vazio. - classificacoes so devia mostrar os que ja fizeram alguma coisa - QFactory.generate() devia fazer run da gen_async, ou remover. diff --git a/aprendizations/__init__.py b/aprendizations/__init__.py index 326c4a2..338c9d8 100644 --- a/aprendizations/__init__.py +++ b/aprendizations/__init__.py @@ -30,7 +30,7 @@ are progressively uncovered as the students progress. ''' APP_NAME = 'aprendizations' -APP_VERSION = '2019.07.dev3' +APP_VERSION = '2019.10.dev1' APP_DESCRIPTION = __doc__ __author__ = 'Miguel Barão' diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index fad500a..274eb76 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -67,18 +67,24 @@ class LearnApp(object): config: Dict[str, Any] = load_yaml(courses) - # courses dict + # --- courses dict self.courses = config['courses'] logger.info(f'Courses: {", ".join(self.courses.keys())}') - # topic dependencies are shared between all courses + # --- topic dependencies are shared between all courses self.deps = nx.DiGraph(prefix=prefix) logger.info('Populating graph:') - for f in config['deps']: - self.populate_graph(f) + + t = config.get('topics', {}) # topics defined directly in courses file + self.populate_graph(t) + logger.info(f'{len(t):>6} topics in {courses}') + for f in config.get('topics_from', []): + c = load_yaml(f) # course configuration + logger.info(f'{len(c["topics"]):>6} topics from {f}') + self.populate_graph(c) logger.info(f'Graph has {len(self.deps)} topics') - # factory is a dict with question generators for all topics + # --- factory is a dict with question generators for all topics self.factory: Dict[str, QFactory] = self.make_factory() # if graph has topics that are not in the database, add them @@ -322,29 +328,30 @@ class LearnApp(object): # Populates a digraph. # # Nodes are the topic references e.g. 'my/topic' - # g.node['my/topic']['name'] name of the topic - # g.node['my/topic']['questions'] list of question refs + # g.nodes['my/topic']['name'] name of the topic + # g.nodes['my/topic']['questions'] list of question refs # # Edges are obtained from the deps defined in the YAML file for each topic. # ------------------------------------------------------------------------ - def populate_graph(self, conffile: str) -> None: - config: Dict[str, Any] = load_yaml(conffile) # course configuration - - # default attributes that apply to the topics - default_file: str = config.get('file', 'questions.yaml') - default_shuffle_questions: bool = config.get('shuffle_questions', True) - default_choose: int = config.get('choose', 9999) - default_forgetting_factor: float = config.get('forgetting_factor', 1.0) - default_maxtries: int = config.get('max_tries', 1) - default_append_wrong: bool = config.get('append_wrong', True) - default_min_level: float = config.get('min_level', 0.01) # to unlock + def populate_graph(self, config: Dict[str, Any]) -> None: + g = self.deps # dependency graph + defaults = { + 'type': 'topic', + 'file': 'questions.yaml', + 'shuffle_questions': True, + 'choose': 9999, + 'forgetting_factor': 1.0, # no forgetting + 'max_tries': 1, # in every question + 'append_wrong': True, + 'min_level': 0.01, # to unlock topic + } + defaults.update(config.get('defaults', {})) # iterate over topics and populate graph topics: Dict[str, Dict] = config.get('topics', {}) - g = self.deps # dependency graph - g.add_nodes_from(topics.keys()) for tref, attr in topics.items(): + logger.debug(f' + {tref}...') for d in attr.get('deps', []): if d not in g.nodes(): logger.error(f'Topic "{tref}" depends on "{d}" but it ' @@ -353,22 +360,15 @@ class LearnApp(object): else: g.add_edge(d, tref) - t = g.node[tref] # get current topic node - t['type'] = attr.get('type', 'topic') + t = g.nodes[tref] # get current topic node t['name'] = attr.get('name', tref) - t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic - t['file'] = attr.get('file', default_file) # questions.yaml - t['shuffle_questions'] = attr.get('shuffle_questions', - default_shuffle_questions) - t['max_tries'] = attr.get('max_tries', default_maxtries) - t['forgetting_factor'] = attr.get('forgetting_factor', - default_forgetting_factor) - t['min_level'] = attr.get('min_level', default_min_level) - t['choose'] = attr.get('choose', default_choose) - t['append_wrong'] = attr.get('append_wrong', default_append_wrong) t['questions'] = attr.get('questions', []) - logger.info(f'{len(topics):>6} topics in {conffile}') + for k, default in defaults.items(): + t[k] = attr.get(k, default) + + t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic + # ======================================================================== # methods that do not change state (pure functions) @@ -378,12 +378,11 @@ class LearnApp(object): # Buils dictionary of question factories # ------------------------------------------------------------------------ def make_factory(self) -> Dict[str, QFactory]: - logger.info('Building questions factory:') factory: Dict[str, QFactory] = {} g = self.deps for tref in g.nodes(): - t = g.node[tref] + t = g.nodes[tref] # load questions as list of dicts topicpath: str = path.join(g.graph['prefix'], tref) @@ -457,13 +456,8 @@ class LearnApp(object): return self.online[uid]['state'].get_current_course_id() # ------------------------------------------------------------------------ - # def get_title(self) -> str: - # # return self.deps.graph.get('title', '') - # return self. - - # ------------------------------------------------------------------------ def get_topic_name(self, ref: str) -> str: - return self.deps.node[ref]['name'] + return self.deps.nodes[ref]['name'] # ------------------------------------------------------------------------ def get_current_public_dir(self, uid: str) -> str: diff --git a/aprendizations/static/js/topic.js b/aprendizations/static/js/topic.js index 7acd2e3..532af82 100644 --- a/aprendizations/static/js/topic.js +++ b/aprendizations/static/js/topic.js @@ -67,13 +67,13 @@ function new_question(type, question, tries, progress) { }); } - // prevent enter submit. - //input:text, input:radio, input:checkbox - $("body").keyup(function (e) { - if (e.keyCode == 13 && e.shiftKey) { - $("#submit").click(); + // disable enter in some question types (prevent submission on enter) + $("input:text, input:radio, input:checkbox").keydown(function (e) { + if (e.keyCode == 13 || e.keyCode == 169) { + e.preventDefault(); return false; - }}); + } + }); } diff --git a/aprendizations/student.py b/aprendizations/student.py index c098755..8480273 100644 --- a/aprendizations/student.py +++ b/aprendizations/student.py @@ -53,7 +53,7 @@ class StudentState(object): else: logger.debug(f'starting course {course}') self.current_course = course - topics = self.courses[course]['topics'] + topics = self.courses[course]['goals'] self.topic_sequence = self.recommend_topic_sequence(topics) # ------------------------------------------------------------------------ @@ -79,7 +79,7 @@ class StudentState(object): self.correct_answers = 0 self.wrong_answers = 0 - t = self.deps.node[topic] + t = self.deps.nodes[topic] k = t['choose'] if t['shuffle_questions']: questions = random.sample(t['questions'], k=k) @@ -174,7 +174,7 @@ class StudentState(object): now = datetime.now() for tref, s in self.state.items(): dt = now - s['date'] - forgetting_factor = self.deps.node[tref]['forgetting_factor'] + forgetting_factor = self.deps.nodes[tref]['forgetting_factor'] s['level'] *= forgetting_factor ** dt.days # forgetting factor # ------------------------------------------------------------------------ @@ -184,7 +184,7 @@ class StudentState(object): for topic in self.deps.nodes(): if topic not in self.state: # if locked pred = self.deps.predecessors(topic) - min_level = self.deps.node[topic]['min_level'] + min_level = self.deps.nodes[topic]['min_level'] if all(d in self.state and self.state[d]['level'] > min_level for d in pred): # all deps are greater than min_level @@ -206,12 +206,12 @@ class StudentState(object): # ------------------------------------------------------------------------ # compute recommended sequence of topics ['a', 'b', ...] # ------------------------------------------------------------------------ - def recommend_topic_sequence(self, targets: List[str] = []) -> List[str]: + def recommend_topic_sequence(self, goals: List[str] = []) -> List[str]: G = self.deps - ts = set(targets) - for t in targets: + ts = set(goals) + for t in goals: ts.update(nx.ancestors(G, t)) - + # FIXME filter by level done, only level < 50% are included tl = list(nx.topological_sort(G.subgraph(ts))) # sort with unlocked first diff --git a/setup.py b/setup.py index 27480d7..ab66535 100644 --- a/setup.py +++ b/setup.py @@ -20,8 +20,13 @@ setup( include_package_data=True, # install files from MANIFEST.in python_requires='>=3.7.*', install_requires=[ - 'tornado', 'mistune', 'pyyaml', 'pygments', 'sqlalchemy', 'bcrypt', - 'networkx' + 'tornado>=6.0', + 'mistune', + 'pyyaml>=5.1', + 'pygments', + 'sqlalchemy', + 'bcrypt>=3.1', + 'networkx>=2.4' ], entry_points={ 'console_scripts': [ -- libgit2 0.21.2