Commit d823a4d87455beadabebfd26c488199af065fcea
1 parent
b1feb56c
Exists in
master
and in
1 other branch
Lots of changes:
- changes checkbox correct list to the same semantics as radio with interval values in the interval [0,1] - adds many sanity checks to questions. - fix display math centering in radio and checkbox options - fix check for nonexisting goals - fix missing database - fix bug where last question would not show images in the solution
Showing
15 changed files
with
342 additions
and
507 deletions
Show diff stats
BUGS.md
| 1 | 1 | ||
| 2 | # BUGS | 2 | # BUGS |
| 3 | 3 | ||
| 4 | - | ||
| 5 | -Traceback (most recent call last): | ||
| 6 | - File "/home/mjsb/.local/lib/python3.7/site-packages/tornado/web.py", line 1697, in _execute | ||
| 7 | - result = method(*self.path_args, **self.path_kwargs) | ||
| 8 | - File "/home/mjsb/.local/lib/python3.7/site-packages/tornado/web.py", line 3174, in wrapper | ||
| 9 | - return method(self, *args, **kwargs) | ||
| 10 | - File "/usr/home/mjsb/Work/Projects/aprendizations/aprendizations/serve.py", line 213, in get | ||
| 11 | - self.learn.start_course(uid, course) | ||
| 12 | - File "/usr/home/mjsb/Work/Projects/aprendizations/aprendizations/learnapp.py", line 275, in start_course | ||
| 13 | - student.start_course(course) | ||
| 14 | - File "/usr/home/mjsb/Work/Projects/aprendizations/aprendizations/student.py", line 57, in start_course | ||
| 15 | - self.topic_sequence = self.recommend_topic_sequence(topics) | ||
| 16 | - File "/usr/home/mjsb/Work/Projects/aprendizations/aprendizations/student.py", line 216, in recommend_topic_sequence | ||
| 17 | - ts.update(nx.ancestors(G, t)) | ||
| 18 | - File "/home/mjsb/.local/lib/python3.7/site-packages/networkx/algorithms/dag.py", line 92, in ancestors | ||
| 19 | - raise nx.NetworkXError("The node %s is not in the graph." % source) | ||
| 20 | -networkx.exception.NetworkXError: The node programming/languages/pseudo-tcg/functions-produtorio is not in the graph. | ||
| 21 | - | ||
| 22 | - | ||
| 23 | - | ||
| 24 | -- detectar se em courses.yaml falta declarar ficheiro. Por exemplo se houver goals que não estao em lado nenhum. | ||
| 25 | -- se num topico, a ultima pergunta tem imagens, o servidor nao fornece as imagengs porque o current_topic passa a None antes de carregar no botao continuar. O caminho é prefix+None e dá erro. | ||
| 26 | -- registar last_seen e remover os antigos de cada vez que houver um login. | 4 | +- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) |
| 5 | +- nao esta a seguir o max_tries definido no ficheiro de dependencias. | ||
| 27 | - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) | 6 | - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) |
| 28 | -- permite definir goal, mas nao verifica se esta no grafo. rebenta no start_topic. | ||
| 29 | - double click submits twice. | 7 | - double click submits twice. |
| 30 | -- nao esta a seguir o max_tries definido no ficheiro de dependencias. | ||
| 31 | -- impedir que quando students.db não é encontrado, crie um ficheiro vazio. | ||
| 32 | -- classificacoes so devia mostrar os que ja fizeram alguma coisa | ||
| 33 | -- QFactory.generate() devia fazer run da gen_async, ou remover. | ||
| 34 | -- marking all options right in a radio question breaks! | ||
| 35 | -- opcao --prefix devia afectar a base de dados? | ||
| 36 | - duplo clicks no botao "responder" dessincroniza as questões, ver debounce em https://stackoverflow.com/questions/20281546/how-to-prevent-calling-of-en-event-handler-twice-on-fast-clicks | 8 | - duplo clicks no botao "responder" dessincroniza as questões, ver debounce em https://stackoverflow.com/questions/20281546/how-to-prevent-calling-of-en-event-handler-twice-on-fast-clicks |
| 37 | -- quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo) | ||
| 38 | -- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) | ||
| 39 | - devia mostrar timeout para o aluno saber a razao. | 9 | - devia mostrar timeout para o aluno saber a razao. |
| 40 | - permitir configuracao para escolher entre static files locais ou remotos | 10 | - permitir configuracao para escolher entre static files locais ou remotos |
| 41 | -- sqlalchemy.pool.impl.NullPool: Exception during reset or similar | ||
| 42 | -sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. | ||
| 43 | - templates question-*.html tem input hidden question_ref que não é usado. remover? | 11 | - templates question-*.html tem input hidden question_ref que não é usado. remover? |
| 44 | -- guardar o estado a meio de um nível. | ||
| 45 | -- safari as vezes envia dois gets no inicio do topico. nesses casos, a segunda pergunta não é actualizada no browser... o topico tem de ser gerado qd se escolhe o topico em main_topics. O get nao deve alterar o estado. | ||
| 46 | -- click numa opcao checkbox fora da checkbox+label não está a funcionar. | ||
| 47 | - shift-enter não está a funcionar | 12 | - shift-enter não está a funcionar |
| 48 | -- mathjax, formulas $$f(x)$$ nas opções de escolha multipla, não ficam centradas em toda a coluna mas apenas na largura do parágrafo. | ||
| 49 | - default prefix should be obtained from each course (yaml conf)? | 13 | - default prefix should be obtained from each course (yaml conf)? |
| 50 | -- tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. | ||
| 51 | 14 | ||
| 52 | # TODO | 15 | # TODO |
| 53 | 16 | ||
| 17 | +- registar last_seen e remover os antigos de cada vez que houver um login. | ||
| 54 | - indicar qtos topicos faltam (>=50%) para terminar o curso. | 18 | - indicar qtos topicos faltam (>=50%) para terminar o curso. |
| 55 | -- use run_script_async to run run_script using asyncio.run? | ||
| 56 | -- ao fim de 3 tentativas de login, envial email para aluno com link para definir nova password (com timeout de 5 minutos). | 19 | +- ao fim de 3 tentativas com password errada, envia email com nova password. |
| 57 | - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias. | 20 | - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias. |
| 58 | - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos. | 21 | - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos. |
| 59 | - botão não sei... | 22 | - botão não sei... |
| @@ -65,14 +28,23 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in | @@ -65,14 +28,23 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in | ||
| 65 | - tabela com perguntas / quantidade de respostas certas/erradas. | 28 | - tabela com perguntas / quantidade de respostas certas/erradas. |
| 66 | - tabela com topicos / quantidade de estrelas. | 29 | - tabela com topicos / quantidade de estrelas. |
| 67 | - pymips: activar/desactivar instruções | 30 | - pymips: activar/desactivar instruções |
| 68 | -- implementar servidor http com redirect para https. | ||
| 69 | -- ao fim de 3 tentativas com password errada, envia email com nova password. | ||
| 70 | - titulos das perguntas não suportam markdown. | 31 | - titulos das perguntas não suportam markdown. |
| 71 | - pagina report que permita ver tabela alunos/topicos, estatisticas perguntas mais falhadas, tempo médio por pergunta. | 32 | - pagina report que permita ver tabela alunos/topicos, estatisticas perguntas mais falhadas, tempo médio por pergunta. |
| 72 | - normalizar com perguntations. | 33 | - normalizar com perguntations. |
| 73 | 34 | ||
| 74 | # FIXED | 35 | # FIXED |
| 75 | 36 | ||
| 37 | +- checkbox devia ter correct no intervalo [0,1] tal como radio. em caso de desconto a correccção faz 2*x-1. isto permite a mesma semantica nos dois tipos de perguntas. | ||
| 38 | +- marking all options right in a radio question breaks! | ||
| 39 | +- implementar servidor http com redirect para https. | ||
| 40 | +- tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. | ||
| 41 | +- click numa opcao checkbox fora da checkbox+label não está a funcionar. | ||
| 42 | +- mathjax, formulas $$f(x)$$ nas opções de escolha multipla, não ficam centradas em toda a coluna mas apenas na largura do parágrafo. | ||
| 43 | +- QFactory.generate() devia fazer run da gen_async, ou remover. | ||
| 44 | +- classificacoes so devia mostrar os que ja fizeram alguma coisa | ||
| 45 | +- impedir que quando students.db não é encontrado, crie um ficheiro vazio. | ||
| 46 | +- permite definir goal, mas nao verifica se esta no grafo. rebenta no start_topic. | ||
| 47 | +- se num topico, a ultima pergunta tem imagens, o servidor nao fornece as imagengs porque o current_topic passa a None antes de carregar no botao continuar. O caminho é prefix+None e dá erro. | ||
| 76 | - caixas com os cursos não se ajustam bem com ecran estreito. | 48 | - caixas com os cursos não se ajustam bem com ecran estreito. |
| 77 | - obter rankings por curso GET course=course_id | 49 | - obter rankings por curso GET course=course_id |
| 78 | - no curso de linear algebra, as perguntas estao shuffled, mas nao deviam estar... nao esta a obedecer a keyword shuffle. | 50 | - no curso de linear algebra, as perguntas estao shuffled, mas nao deviam estar... nao esta a obedecer a keyword shuffle. |
aprendizations/learnapp.py
| @@ -16,10 +16,11 @@ import sqlalchemy as sa | @@ -16,10 +16,11 @@ import sqlalchemy as sa | ||
| 16 | 16 | ||
| 17 | # this project | 17 | # this project |
| 18 | from .models import Student, Answer, Topic, StudentTopic | 18 | from .models import Student, Answer, Topic, StudentTopic |
| 19 | -from .questions import Question, QFactory, QDict | 19 | +from .questions import Question, QFactory, QDict, QuestionException |
| 20 | from .student import StudentState | 20 | from .student import StudentState |
| 21 | from .tools import load_yaml | 21 | from .tools import load_yaml |
| 22 | 22 | ||
| 23 | + | ||
| 23 | # setup logger for this module | 24 | # setup logger for this module |
| 24 | logger = logging.getLogger(__name__) | 25 | logger = logging.getLogger(__name__) |
| 25 | 26 | ||
| @@ -68,28 +69,36 @@ class LearnApp(object): | @@ -68,28 +69,36 @@ class LearnApp(object): | ||
| 68 | 69 | ||
| 69 | config: Dict[str, Any] = load_yaml(courses) | 70 | config: Dict[str, Any] = load_yaml(courses) |
| 70 | 71 | ||
| 71 | - # --- courses dict | ||
| 72 | - self.courses = config['courses'] | ||
| 73 | - logger.info(f'Courses: {", ".join(self.courses.keys())}') | ||
| 74 | - | ||
| 75 | # --- topic dependencies are shared between all courses | 72 | # --- topic dependencies are shared between all courses |
| 76 | self.deps = nx.DiGraph(prefix=prefix) | 73 | self.deps = nx.DiGraph(prefix=prefix) |
| 77 | - logger.info('Populating graph:') | 74 | + logger.info('Populating topic graph:') |
| 78 | 75 | ||
| 79 | t = config.get('topics', {}) # topics defined directly in courses file | 76 | t = config.get('topics', {}) # topics defined directly in courses file |
| 80 | self.populate_graph(t) | 77 | self.populate_graph(t) |
| 81 | logger.info(f'{len(t):>6} topics in {courses}') | 78 | logger.info(f'{len(t):>6} topics in {courses}') |
| 82 | for f in config.get('topics_from', []): | 79 | for f in config.get('topics_from', []): |
| 83 | c = load_yaml(f) # course configuration | 80 | c = load_yaml(f) # course configuration |
| 84 | - logger.info(f'{len(c["topics"]):>6} topics from {f}') | 81 | + |
| 82 | + # FIXME set defaults?? | ||
| 83 | + logger.info(f'{len(c["topics"]):>6} topics imported from {f}') | ||
| 85 | self.populate_graph(c) | 84 | self.populate_graph(c) |
| 86 | logger.info(f'Graph has {len(self.deps)} topics') | 85 | logger.info(f'Graph has {len(self.deps)} topics') |
| 87 | 86 | ||
| 87 | + # --- courses dict | ||
| 88 | + self.courses = config['courses'] | ||
| 89 | + logger.info(f'Courses: {", ".join(self.courses.keys())}') | ||
| 90 | + for c, d in self.courses.items(): | ||
| 91 | + for goal in d['goals']: | ||
| 92 | + if goal not in self.deps.nodes(): | ||
| 93 | + # logger.error(f'Goal "{goal}" of "{c}"" not in the graph') | ||
| 94 | + raise LearnException(f'Goal "{goal}" from course "{c}" ' | ||
| 95 | + ' does not exist') | ||
| 96 | + | ||
| 88 | # --- factory is a dict with question generators for all topics | 97 | # --- factory is a dict with question generators for all topics |
| 89 | self.factory: Dict[str, QFactory] = self.make_factory() | 98 | self.factory: Dict[str, QFactory] = self.make_factory() |
| 90 | 99 | ||
| 91 | # if graph has topics that are not in the database, add them | 100 | # if graph has topics that are not in the database, add them |
| 92 | - self.db_add_missing_topics(self.deps.nodes()) | 101 | + self.add_missing_topics(self.deps.nodes()) |
| 93 | 102 | ||
| 94 | if check: | 103 | if check: |
| 95 | self.sanity_check_questions() | 104 | self.sanity_check_questions() |
| @@ -103,8 +112,8 @@ class LearnApp(object): | @@ -103,8 +112,8 @@ class LearnApp(object): | ||
| 103 | logger.debug(f'checking {qref}...') | 112 | logger.debug(f'checking {qref}...') |
| 104 | try: | 113 | try: |
| 105 | q = self.factory[qref].generate() | 114 | q = self.factory[qref].generate() |
| 106 | - except Exception: | ||
| 107 | - logger.error(f'Failed to generate "{qref}".') | 115 | + except QuestionException as e: |
| 116 | + logger.error(e) | ||
| 108 | errors += 1 | 117 | errors += 1 |
| 109 | continue # to next question | 118 | continue # to next question |
| 110 | 119 | ||
| @@ -127,7 +136,7 @@ class LearnApp(object): | @@ -127,7 +136,7 @@ class LearnApp(object): | ||
| 127 | continue # to next test | 136 | continue # to next test |
| 128 | 137 | ||
| 129 | if errors > 0: | 138 | if errors > 0: |
| 130 | - logger.error(f'{errors:>6} errors found.') | 139 | + logger.error(f'{errors:>6} error(s) found.') |
| 131 | raise LearnException('Sanity checks') | 140 | raise LearnException('Sanity checks') |
| 132 | else: | 141 | else: |
| 133 | logger.info(' 0 errors found.') | 142 | logger.info(' 0 errors found.') |
| @@ -216,13 +225,13 @@ class LearnApp(object): | @@ -216,13 +225,13 @@ class LearnApp(object): | ||
| 216 | return True | 225 | return True |
| 217 | 226 | ||
| 218 | # ------------------------------------------------------------------------ | 227 | # ------------------------------------------------------------------------ |
| 219 | - # checks answer (updating student state) and returns grade. FIXME type of answer | 228 | + # Checks answer and update database. Returns corrected question. |
| 220 | # ------------------------------------------------------------------------ | 229 | # ------------------------------------------------------------------------ |
| 221 | - async def check_answer(self, uid: str, answer) -> Tuple[Question, str]: | 230 | + async def check_answer(self, uid: str, answer) -> Question: |
| 222 | student = self.online[uid]['state'] | 231 | student = self.online[uid]['state'] |
| 223 | topic = student.get_current_topic() | 232 | topic = student.get_current_topic() |
| 224 | 233 | ||
| 225 | - q, action = await student.check_answer(answer) # may move questions | 234 | + q = await student.check_answer(answer) |
| 226 | 235 | ||
| 227 | logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') | 236 | logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') |
| 228 | 237 | ||
| @@ -237,8 +246,8 @@ class LearnApp(object): | @@ -237,8 +246,8 @@ class LearnApp(object): | ||
| 237 | topic_id=topic)) | 246 | topic_id=topic)) |
| 238 | logger.debug(f'db insert answer of {q["ref"]}') | 247 | logger.debug(f'db insert answer of {q["ref"]}') |
| 239 | 248 | ||
| 249 | + # save topic if finished | ||
| 240 | if student.topic_has_finished(): | 250 | if student.topic_has_finished(): |
| 241 | - # finished topic, save into database | ||
| 242 | logger.info(f'User "{uid}" finished "{topic}"') | 251 | logger.info(f'User "{uid}" finished "{topic}"') |
| 243 | level: float = student.get_topic_level(topic) | 252 | level: float = student.get_topic_level(topic) |
| 244 | date: str = str(student.get_topic_date(topic)) | 253 | date: str = str(student.get_topic_date(topic)) |
| @@ -264,7 +273,13 @@ class LearnApp(object): | @@ -264,7 +273,13 @@ class LearnApp(object): | ||
| 264 | 273 | ||
| 265 | s.add(a) | 274 | s.add(a) |
| 266 | 275 | ||
| 267 | - return q, action | 276 | + return q |
| 277 | + | ||
| 278 | + # ------------------------------------------------------------------------ | ||
| 279 | + # get the question to show (current or new one) | ||
| 280 | + # ------------------------------------------------------------------------ | ||
| 281 | + async def get_question(self, uid: str) -> Optional[Question]: | ||
| 282 | + return await self.online[uid]['state'].get_question() | ||
| 268 | 283 | ||
| 269 | # ------------------------------------------------------------------------ | 284 | # ------------------------------------------------------------------------ |
| 270 | # Start course | 285 | # Start course |
| @@ -277,7 +292,7 @@ class LearnApp(object): | @@ -277,7 +292,7 @@ class LearnApp(object): | ||
| 277 | logger.warning(f'"{uid}" could not start course "{course}": {e}') | 292 | logger.warning(f'"{uid}" could not start course "{course}": {e}') |
| 278 | raise | 293 | raise |
| 279 | else: | 294 | else: |
| 280 | - logger.info(f'"{uid}" started course "{course}"') | 295 | + logger.info(f'User "{uid}" started course "{course}"') |
| 281 | 296 | ||
| 282 | # ------------------------------------------------------------------------ | 297 | # ------------------------------------------------------------------------ |
| 283 | # Start new topic | 298 | # Start new topic |
| @@ -294,7 +309,7 @@ class LearnApp(object): | @@ -294,7 +309,7 @@ class LearnApp(object): | ||
| 294 | # ------------------------------------------------------------------------ | 309 | # ------------------------------------------------------------------------ |
| 295 | # Fill db table 'Topic' with topics from the graph if not already there. | 310 | # Fill db table 'Topic' with topics from the graph if not already there. |
| 296 | # ------------------------------------------------------------------------ | 311 | # ------------------------------------------------------------------------ |
| 297 | - def db_add_missing_topics(self, topics: List[str]) -> None: | 312 | + def add_missing_topics(self, topics: List[str]) -> None: |
| 298 | with self.db_session() as s: | 313 | with self.db_session() as s: |
| 299 | new_topics = [Topic(id=t) for t in topics | 314 | new_topics = [Topic(id=t) for t in topics |
| 300 | if (t,) not in s.query(Topic.id)] | 315 | if (t,) not in s.query(Topic.id)] |
| @@ -310,6 +325,10 @@ class LearnApp(object): | @@ -310,6 +325,10 @@ class LearnApp(object): | ||
| 310 | def db_setup(self, db: str) -> None: | 325 | def db_setup(self, db: str) -> None: |
| 311 | 326 | ||
| 312 | logger.info(f'Checking database "{db}":') | 327 | logger.info(f'Checking database "{db}":') |
| 328 | + if not path.exists(db): | ||
| 329 | + raise LearnException('Database does not exist. ' | ||
| 330 | + 'Use "initdb-aprendizations" to create') | ||
| 331 | + | ||
| 313 | engine = sa.create_engine(f'sqlite:///{db}', echo=False) | 332 | engine = sa.create_engine(f'sqlite:///{db}', echo=False) |
| 314 | self.Session = sa.orm.sessionmaker(bind=engine) | 333 | self.Session = sa.orm.sessionmaker(bind=engine) |
| 315 | try: | 334 | try: |
| @@ -325,7 +344,7 @@ class LearnApp(object): | @@ -325,7 +344,7 @@ class LearnApp(object): | ||
| 325 | logger.info(f'{m:6} topics') | 344 | logger.info(f'{m:6} topics') |
| 326 | logger.info(f'{q:6} answers') | 345 | logger.info(f'{q:6} answers') |
| 327 | 346 | ||
| 328 | - # ============================================================================ | 347 | + # ======================================================================== |
| 329 | # Populates a digraph. | 348 | # Populates a digraph. |
| 330 | # | 349 | # |
| 331 | # Nodes are the topic references e.g. 'my/topic' | 350 | # Nodes are the topic references e.g. 'my/topic' |
| @@ -380,7 +399,7 @@ class LearnApp(object): | @@ -380,7 +399,7 @@ class LearnApp(object): | ||
| 380 | def make_factory(self) -> Dict[str, QFactory]: | 399 | def make_factory(self) -> Dict[str, QFactory]: |
| 381 | 400 | ||
| 382 | logger.info('Building questions factory:') | 401 | logger.info('Building questions factory:') |
| 383 | - factory: Dict[str, QFactory] = {} | 402 | + factory = {} |
| 384 | g = self.deps | 403 | g = self.deps |
| 385 | for tref in g.nodes(): | 404 | for tref in g.nodes(): |
| 386 | t = g.nodes[tref] | 405 | t = g.nodes[tref] |
| @@ -389,6 +408,7 @@ class LearnApp(object): | @@ -389,6 +408,7 @@ class LearnApp(object): | ||
| 389 | topicpath: str = path.join(g.graph['prefix'], tref) | 408 | topicpath: str = path.join(g.graph['prefix'], tref) |
| 390 | fullpath: str = path.join(topicpath, t['file']) | 409 | fullpath: str = path.join(topicpath, t['file']) |
| 391 | 410 | ||
| 411 | + logger.debug(f' Loading {fullpath}') | ||
| 392 | questions: List[QDict] = load_yaml(fullpath, default=[]) | 412 | questions: List[QDict] = load_yaml(fullpath, default=[]) |
| 393 | 413 | ||
| 394 | # update refs to include topic as prefix. | 414 | # update refs to include topic as prefix. |
| @@ -397,7 +417,7 @@ class LearnApp(object): | @@ -397,7 +417,7 @@ class LearnApp(object): | ||
| 397 | # within the file | 417 | # within the file |
| 398 | for i, q in enumerate(questions): | 418 | for i, q in enumerate(questions): |
| 399 | qref = q.get('ref', str(i)) # ref or number | 419 | qref = q.get('ref', str(i)) # ref or number |
| 400 | - q['ref'] = tref + ':' + qref | 420 | + q['ref'] = f'{tref}:{qref}' |
| 401 | q['path'] = topicpath | 421 | q['path'] = topicpath |
| 402 | q.setdefault('append_wrong', t['append_wrong']) | 422 | q.setdefault('append_wrong', t['append_wrong']) |
| 403 | 423 | ||
| @@ -410,10 +430,11 @@ class LearnApp(object): | @@ -410,10 +430,11 @@ class LearnApp(object): | ||
| 410 | for q in questions: | 430 | for q in questions: |
| 411 | if q['ref'] in t['questions']: | 431 | if q['ref'] in t['questions']: |
| 412 | factory[q['ref']] = QFactory(q) | 432 | factory[q['ref']] = QFactory(q) |
| 433 | + logger.debug(f' + {q["ref"]}') | ||
| 413 | 434 | ||
| 414 | - logger.info(f'{len(t["questions"]):6} {tref}') | 435 | + logger.info(f'{len(t["questions"]):6} questions in {tref}') |
| 415 | 436 | ||
| 416 | - logger.info(f'Factory contains {len(factory)} questions') | 437 | + logger.info(f'Factory has {len(factory)} questions') |
| 417 | return factory | 438 | return factory |
| 418 | 439 | ||
| 419 | # ------------------------------------------------------------------------ | 440 | # ------------------------------------------------------------------------ |
aprendizations/main.py
| @@ -9,7 +9,7 @@ import sys | @@ -9,7 +9,7 @@ import sys | ||
| 9 | from typing import Any, Dict | 9 | from typing import Any, Dict |
| 10 | 10 | ||
| 11 | # this project | 11 | # this project |
| 12 | -from .learnapp import LearnApp, DatabaseUnusableError | 12 | +from .learnapp import LearnApp, DatabaseUnusableError, LearnException |
| 13 | from .serve import run_webserver | 13 | from .serve import run_webserver |
| 14 | from .tools import load_yaml | 14 | from .tools import load_yaml |
| 15 | from . import APP_NAME, APP_VERSION | 15 | from . import APP_NAME, APP_VERSION |
| @@ -160,7 +160,7 @@ def main(): | @@ -160,7 +160,7 @@ def main(): | ||
| 160 | else: | 160 | else: |
| 161 | logging.info('SSL certificates loaded') | 161 | logging.info('SSL certificates loaded') |
| 162 | 162 | ||
| 163 | - # --- start application | 163 | + # --- start application -------------------------------------------------- |
| 164 | try: | 164 | try: |
| 165 | learnapp = LearnApp(courses=arg.courses, | 165 | learnapp = LearnApp(courses=arg.courses, |
| 166 | prefix=arg.prefix, | 166 | prefix=arg.prefix, |
| @@ -178,13 +178,16 @@ def main(): | @@ -178,13 +178,16 @@ def main(): | ||
| 178 | '--------------------------------------------------------------', | 178 | '--------------------------------------------------------------', |
| 179 | sep='\n') | 179 | sep='\n') |
| 180 | sys.exit(1) | 180 | sys.exit(1) |
| 181 | + except LearnException as e: | ||
| 182 | + logging.critical(e) | ||
| 183 | + sys.exit(1) | ||
| 181 | except Exception: | 184 | except Exception: |
| 182 | logging.critical('Failed to start backend.') | 185 | logging.critical('Failed to start backend.') |
| 183 | sys.exit(1) | 186 | sys.exit(1) |
| 184 | else: | 187 | else: |
| 185 | - logging.info('Backend started') | 188 | + logging.info('LearnApp started') |
| 186 | 189 | ||
| 187 | - # --- run webserver forever | 190 | + # --- run webserver forever ---------------------------------------------- |
| 188 | run_webserver(app=learnapp, ssl=ssl_ctx, port=arg.port, debug=arg.debug) | 191 | run_webserver(app=learnapp, ssl=ssl_ctx, port=arg.port, debug=arg.debug) |
| 189 | 192 | ||
| 190 | 193 |
aprendizations/questions.py
| @@ -42,7 +42,6 @@ class Question(dict): | @@ -42,7 +42,6 @@ class Question(dict): | ||
| 42 | 'comments': '', | 42 | 'comments': '', |
| 43 | 'solution': '', | 43 | 'solution': '', |
| 44 | 'files': {}, | 44 | 'files': {}, |
| 45 | - # 'max_tries': 3, | ||
| 46 | })) | 45 | })) |
| 47 | 46 | ||
| 48 | def correct(self) -> None: | 47 | def correct(self) -> None: |
| @@ -72,7 +71,6 @@ class QuestionRadio(Question): | @@ -72,7 +71,6 @@ class QuestionRadio(Question): | ||
| 72 | ''' | 71 | ''' |
| 73 | 72 | ||
| 74 | # ------------------------------------------------------------------------ | 73 | # ------------------------------------------------------------------------ |
| 75 | - # FIXME marking all options right breaks | ||
| 76 | def __init__(self, q: QDict) -> None: | 74 | def __init__(self, q: QDict) -> None: |
| 77 | super().__init__(q) | 75 | super().__init__(q) |
| 78 | 76 | ||
| @@ -86,18 +84,46 @@ class QuestionRadio(Question): | @@ -86,18 +84,46 @@ class QuestionRadio(Question): | ||
| 86 | 'max_tries': (n + 3) // 4 # 1 try for each 4 options | 84 | 'max_tries': (n + 3) // 4 # 1 try for each 4 options |
| 87 | })) | 85 | })) |
| 88 | 86 | ||
| 89 | - # convert int to list, e.g. correct: 2 --> correct: [0,0,1,0,0] | ||
| 90 | - # correctness levels from 0.0 to 1.0 (no discount here!) | 87 | + # check correct bounds and convert int to list, |
| 88 | + # e.g. correct: 2 --> correct: [0,0,1,0,0] | ||
| 91 | if isinstance(self['correct'], int): | 89 | if isinstance(self['correct'], int): |
| 90 | + if not (0 <= self['correct'] < n): | ||
| 91 | + msg = (f'Correct option not in range 0..{n-1} in ' | ||
| 92 | + f'"{self["ref"]}"') | ||
| 93 | + raise QuestionException(msg) | ||
| 94 | + | ||
| 92 | self['correct'] = [1.0 if x == self['correct'] else 0.0 | 95 | self['correct'] = [1.0 if x == self['correct'] else 0.0 |
| 93 | for x in range(n)] | 96 | for x in range(n)] |
| 94 | 97 | ||
| 95 | - if len(self['correct']) != n: | ||
| 96 | - msg = ('Number of options and correct differ in ' | ||
| 97 | - f'"{self["ref"]}", file "{self["filename"]}".') | ||
| 98 | - logger.error(msg) | ||
| 99 | - raise QuestionException(msg) | ||
| 100 | - | 98 | + elif isinstance(self['correct'], list): |
| 99 | + # must match number of options | ||
| 100 | + if len(self['correct']) != n: | ||
| 101 | + msg = (f'Incompatible sizes: {n} options vs ' | ||
| 102 | + f'{len(self["correct"])} correct in "{self["ref"]}"') | ||
| 103 | + raise QuestionException(msg) | ||
| 104 | + # make sure is a list of floats | ||
| 105 | + try: | ||
| 106 | + self['correct'] = [float(x) for x in self['correct']] | ||
| 107 | + except (ValueError, TypeError): | ||
| 108 | + msg = (f'Correct list must contain numbers [0.0, 1.0] or ' | ||
| 109 | + f'booleans in "{self["ref"]}"') | ||
| 110 | + raise QuestionException(msg) | ||
| 111 | + | ||
| 112 | + # check grade boundaries | ||
| 113 | + if self['discount'] and not all(0.0 <= x <= 1.0 | ||
| 114 | + for x in self['correct']): | ||
| 115 | + msg = (f'Correct values must be in the interval [0.0, 1.0] in ' | ||
| 116 | + f'"{self["ref"]}"') | ||
| 117 | + raise QuestionException(msg) | ||
| 118 | + | ||
| 119 | + # at least one correct option | ||
| 120 | + if all(x < 1.0 for x in self['correct']): | ||
| 121 | + msg = (f'At least one correct option is required in ' | ||
| 122 | + f'"{self["ref"]}"') | ||
| 123 | + raise QuestionException(msg) | ||
| 124 | + | ||
| 125 | + # If shuffle==false, all options are shown as defined | ||
| 126 | + # otherwise, select 1 correct and choose a few wrong ones | ||
| 101 | if self['shuffle']: | 127 | if self['shuffle']: |
| 102 | # lists with indices of right and wrong options | 128 | # lists with indices of right and wrong options |
| 103 | right = [i for i in range(n) if self['correct'][i] >= 1] | 129 | right = [i for i in range(n) if self['correct'][i] >= 1] |
| @@ -123,7 +149,7 @@ class QuestionRadio(Question): | @@ -123,7 +149,7 @@ class QuestionRadio(Question): | ||
| 123 | # final shuffle of the options | 149 | # final shuffle of the options |
| 124 | perm = random.sample(range(self['choose']), k=self['choose']) | 150 | perm = random.sample(range(self['choose']), k=self['choose']) |
| 125 | self['options'] = [str(options[i]) for i in perm] | 151 | self['options'] = [str(options[i]) for i in perm] |
| 126 | - self['correct'] = [float(correct[i]) for i in perm] | 152 | + self['correct'] = [correct[i] for i in perm] |
| 127 | 153 | ||
| 128 | # ------------------------------------------------------------------------ | 154 | # ------------------------------------------------------------------------ |
| 129 | # can assign negative grades for wrong answers | 155 | # can assign negative grades for wrong answers |
| @@ -131,10 +157,13 @@ class QuestionRadio(Question): | @@ -131,10 +157,13 @@ class QuestionRadio(Question): | ||
| 131 | super().correct() | 157 | super().correct() |
| 132 | 158 | ||
| 133 | if self['answer'] is not None: | 159 | if self['answer'] is not None: |
| 134 | - x = self['correct'][int(self['answer'])] | ||
| 135 | - if self['discount']: | ||
| 136 | - n = len(self['options']) # number of options | ||
| 137 | - x_aver = sum(self['correct']) / n | 160 | + x = self['correct'][int(self['answer'])] # get grade of the answer |
| 161 | + n = len(self['options']) | ||
| 162 | + x_aver = sum(self['correct']) / n # expected value of grade | ||
| 163 | + | ||
| 164 | + # note: there are no numerical errors when summing 1.0s so the | ||
| 165 | + # x_aver can be exactly 1.0 if all options are right | ||
| 166 | + if self['discount'] and x_aver != 1.0: | ||
| 138 | x = (x - x_aver) / (1.0 - x_aver) | 167 | x = (x - x_aver) / (1.0 - x_aver) |
| 139 | self['grade'] = x | 168 | self['grade'] = x |
| 140 | 169 | ||
| @@ -168,12 +197,44 @@ class QuestionCheckbox(Question): | @@ -168,12 +197,44 @@ class QuestionCheckbox(Question): | ||
| 168 | 'max_tries': max(1, min(n - 1, 3)) | 197 | 'max_tries': max(1, min(n - 1, 3)) |
| 169 | })) | 198 | })) |
| 170 | 199 | ||
| 200 | + # must be a list of numbers | ||
| 201 | + if not isinstance(self['correct'], list): | ||
| 202 | + msg = 'Correct must be a list of numbers or booleans' | ||
| 203 | + raise QuestionException(msg) | ||
| 204 | + | ||
| 205 | + # must match number of options | ||
| 171 | if len(self['correct']) != n: | 206 | if len(self['correct']) != n: |
| 172 | - msg = (f'Options and correct size mismatch in ' | ||
| 173 | - f'"{self["ref"]}", file "{self["filename"]}".') | ||
| 174 | - logger.error(msg) | 207 | + msg = (f'Incompatible sizes: {n} options vs ' |
| 208 | + f'{len(self["correct"])} correct in "{self["ref"]}"') | ||
| 209 | + raise QuestionException(msg) | ||
| 210 | + | ||
| 211 | + # make sure is a list of floats | ||
| 212 | + try: | ||
| 213 | + self['correct'] = [float(x) for x in self['correct']] | ||
| 214 | + except (ValueError, TypeError): | ||
| 215 | + msg = (f'Correct list must contain numbers or ' | ||
| 216 | + f'booleans in "{self["ref"]}"') | ||
| 175 | raise QuestionException(msg) | 217 | raise QuestionException(msg) |
| 176 | 218 | ||
| 219 | + # check grade boundaries | ||
| 220 | + if self['discount'] and not all(0.0 <= x <= 1.0 | ||
| 221 | + for x in self['correct']): | ||
| 222 | + | ||
| 223 | + msg0 = ('+-------------- BEHAVIOR CHANGE NOTICE --------------+') | ||
| 224 | + msg1 = ('| Correct values must be in the interval [0.0, 1.0]. |') | ||
| 225 | + msg2 = ('| I will convert to the new behavior, but you should |') | ||
| 226 | + msg3 = ('| fix it in the question. |') | ||
| 227 | + msg4 = ('+----------------------------------------------------+') | ||
| 228 | + logger.warning(msg0) | ||
| 229 | + logger.warning(msg1) | ||
| 230 | + logger.warning(msg2) | ||
| 231 | + logger.warning(msg3) | ||
| 232 | + logger.warning(msg4) | ||
| 233 | + logger.warning(f'please fix "{self["ref"]}"') | ||
| 234 | + | ||
| 235 | + # normalize to [0,1] | ||
| 236 | + self['correct'] = [(x+1)/2 for x in self['correct']] | ||
| 237 | + | ||
| 177 | # if an option is a list of (right, wrong), pick one | 238 | # if an option is a list of (right, wrong), pick one |
| 178 | options = [] | 239 | options = [] |
| 179 | correct = [] | 240 | correct = [] |
| @@ -182,9 +243,10 @@ class QuestionCheckbox(Question): | @@ -182,9 +243,10 @@ class QuestionCheckbox(Question): | ||
| 182 | r = random.randint(0, 1) | 243 | r = random.randint(0, 1) |
| 183 | o = o[r] | 244 | o = o[r] |
| 184 | if r == 1: | 245 | if r == 1: |
| 185 | - c = -c | 246 | + # c = -c |
| 247 | + c = 1.0 - c | ||
| 186 | options.append(str(o)) | 248 | options.append(str(o)) |
| 187 | - correct.append(float(c)) | 249 | + correct.append(c) |
| 188 | 250 | ||
| 189 | # generate random permutation, e.g. [2,1,4,0,3] | 251 | # generate random permutation, e.g. [2,1,4,0,3] |
| 190 | # and apply to `options` and `correct` | 252 | # and apply to `options` and `correct` |
| @@ -202,21 +264,20 @@ class QuestionCheckbox(Question): | @@ -202,21 +264,20 @@ class QuestionCheckbox(Question): | ||
| 202 | super().correct() | 264 | super().correct() |
| 203 | 265 | ||
| 204 | if self['answer'] is not None: | 266 | if self['answer'] is not None: |
| 205 | - sum_abs = sum(abs(p) for p in self['correct']) | ||
| 206 | - if sum_abs < 1e-6: # case correct [0,...,0] avoid div-by-zero | ||
| 207 | - self['grade'] = 1.0 | ||
| 208 | - | 267 | + x = 0.0 |
| 268 | + if self['discount']: | ||
| 269 | + sum_abs = sum(abs(2*p-1) for p in self['correct']) | ||
| 270 | + for i, p in enumerate(self['correct']): | ||
| 271 | + x += 2*p-1 if str(i) in self['answer'] else 1-2*p | ||
| 209 | else: | 272 | else: |
| 210 | - x = 0.0 | ||
| 211 | - | ||
| 212 | - if self['discount']: | ||
| 213 | - for i, p in enumerate(self['correct']): | ||
| 214 | - x += p if str(i) in self['answer'] else -p | ||
| 215 | - else: | ||
| 216 | - for i, p in enumerate(self['correct']): | ||
| 217 | - x += p if str(i) in self['answer'] else 0.0 | 273 | + sum_abs = sum(abs(p) for p in self['correct']) |
| 274 | + for i, p in enumerate(self['correct']): | ||
| 275 | + x += p if str(i) in self['answer'] else 0.0 | ||
| 218 | 276 | ||
| 277 | + try: | ||
| 219 | self['grade'] = x / sum_abs | 278 | self['grade'] = x / sum_abs |
| 279 | + except ZeroDivisionError: | ||
| 280 | + self['grade'] = 1.0 # limit p->0 | ||
| 220 | 281 | ||
| 221 | 282 | ||
| 222 | # ============================================================================ | 283 | # ============================================================================ |
| @@ -240,15 +301,21 @@ class QuestionText(Question): | @@ -240,15 +301,21 @@ class QuestionText(Question): | ||
| 240 | 301 | ||
| 241 | # make sure its always a list of possible correct answers | 302 | # make sure its always a list of possible correct answers |
| 242 | if not isinstance(self['correct'], list): | 303 | if not isinstance(self['correct'], list): |
| 243 | - self['correct'] = [self['correct']] | 304 | + self['correct'] = [str(self['correct'])] |
| 305 | + else: | ||
| 306 | + # make sure all elements of the list are strings | ||
| 307 | + self['correct'] = [str(a) for a in self['correct']] | ||
| 244 | 308 | ||
| 245 | - # make sure all elements of the list are strings | ||
| 246 | - self['correct'] = [str(a) for a in self['correct']] | 309 | + for f in self['transform']: |
| 310 | + if f not in ('remove_space', 'trim', 'normalize_space', 'lower', | ||
| 311 | + 'upper'): | ||
| 312 | + msg = (f'Unknown transform "{f}" in "{self["ref"]}"') | ||
| 313 | + raise QuestionException(msg) | ||
| 247 | 314 | ||
| 248 | - # make sure that the answers are invariant with respect to the filters | 315 | + # check if answers are invariant with respect to the transforms |
| 249 | if any(c != self.transform(c) for c in self['correct']): | 316 | if any(c != self.transform(c) for c in self['correct']): |
| 250 | logger.warning(f'in "{self["ref"]}", correct answers are not ' | 317 | logger.warning(f'in "{self["ref"]}", correct answers are not ' |
| 251 | - 'invariant wrt transformations') | 318 | + 'invariant wrt transformations => never correct') |
| 252 | 319 | ||
| 253 | # ------------------------------------------------------------------------ | 320 | # ------------------------------------------------------------------------ |
| 254 | # apply optional filters to the answer | 321 | # apply optional filters to the answer |
| @@ -302,8 +369,12 @@ class QuestionTextRegex(Question): | @@ -302,8 +369,12 @@ class QuestionTextRegex(Question): | ||
| 302 | if not isinstance(self['correct'], list): | 369 | if not isinstance(self['correct'], list): |
| 303 | self['correct'] = [self['correct']] | 370 | self['correct'] = [self['correct']] |
| 304 | 371 | ||
| 305 | - # make sure all elements of the list are strings | ||
| 306 | - self['correct'] = [str(a) for a in self['correct']] | 372 | + # converts patterns to compiled versions |
| 373 | + try: | ||
| 374 | + self['correct'] = [re.compile(a) for a in self['correct']] | ||
| 375 | + except Exception: | ||
| 376 | + msg = f'Failed to compile regex in "{self["ref"]}"' | ||
| 377 | + raise QuestionException(msg) | ||
| 307 | 378 | ||
| 308 | # ------------------------------------------------------------------------ | 379 | # ------------------------------------------------------------------------ |
| 309 | def correct(self) -> None: | 380 | def correct(self) -> None: |
| @@ -312,12 +383,12 @@ class QuestionTextRegex(Question): | @@ -312,12 +383,12 @@ class QuestionTextRegex(Question): | ||
| 312 | self['grade'] = 0.0 | 383 | self['grade'] = 0.0 |
| 313 | for r in self['correct']: | 384 | for r in self['correct']: |
| 314 | try: | 385 | try: |
| 315 | - if re.match(r, self['answer']): | 386 | + if r.match(self['answer']): |
| 316 | self['grade'] = 1.0 | 387 | self['grade'] = 1.0 |
| 317 | return | 388 | return |
| 318 | except TypeError: | 389 | except TypeError: |
| 319 | - logger.error(f'While matching regex {self["correct"]} with' | ||
| 320 | - f' answer "{self["answer"]}".') | 390 | + logger.error(f'While matching regex {r.pattern} with ' |
| 391 | + f'answer "{self["answer"]}".') | ||
| 321 | 392 | ||
| 322 | 393 | ||
| 323 | # ============================================================================ | 394 | # ============================================================================ |
| @@ -339,6 +410,30 @@ class QuestionNumericInterval(Question): | @@ -339,6 +410,30 @@ class QuestionNumericInterval(Question): | ||
| 339 | 'correct': [1.0, -1.0], # will always return false | 410 | 'correct': [1.0, -1.0], # will always return false |
| 340 | })) | 411 | })) |
| 341 | 412 | ||
| 413 | + # if only one number n is given, make an interval [n,n] | ||
| 414 | + if isinstance(self['correct'], (int, float)): | ||
| 415 | + self['correct'] = [float(self['correct']), float(self['correct'])] | ||
| 416 | + | ||
| 417 | + # make sure its a list of two numbers | ||
| 418 | + elif isinstance(self['correct'], list): | ||
| 419 | + if len(self['correct']) != 2: | ||
| 420 | + msg = (f'Numeric interval must be a list with two numbers, in ' | ||
| 421 | + f'{self["ref"]}') | ||
| 422 | + raise QuestionException(msg) | ||
| 423 | + | ||
| 424 | + try: | ||
| 425 | + self['correct'] = [float(n) for n in self['correct']] | ||
| 426 | + except Exception: | ||
| 427 | + msg = (f'Numeric interval must be a list with two numbers, in ' | ||
| 428 | + f'{self["ref"]}') | ||
| 429 | + raise QuestionException(msg) | ||
| 430 | + | ||
| 431 | + # invalid | ||
| 432 | + else: | ||
| 433 | + msg = (f'Numeric interval must be a list with two numbers, in ' | ||
| 434 | + f'{self["ref"]}') | ||
| 435 | + raise QuestionException(msg) | ||
| 436 | + | ||
| 342 | # ------------------------------------------------------------------------ | 437 | # ------------------------------------------------------------------------ |
| 343 | def correct(self) -> None: | 438 | def correct(self) -> None: |
| 344 | super().correct() | 439 | super().correct() |
| @@ -520,23 +615,27 @@ class QFactory(object): | @@ -520,23 +615,27 @@ class QFactory(object): | ||
| 520 | if q['type'] == 'generator': | 615 | if q['type'] == 'generator': |
| 521 | logger.debug(f' \\_ Running "{q["script"]}".') | 616 | logger.debug(f' \\_ Running "{q["script"]}".') |
| 522 | q.setdefault('args', []) | 617 | q.setdefault('args', []) |
| 523 | - q.setdefault('stdin', '') # FIXME is it really necessary? | 618 | + q.setdefault('stdin', '') |
| 524 | script = path.join(q['path'], q['script']) | 619 | script = path.join(q['path'], q['script']) |
| 525 | out = await run_script_async(script=script, args=q['args'], | 620 | out = await run_script_async(script=script, args=q['args'], |
| 526 | stdin=q['stdin']) | 621 | stdin=q['stdin']) |
| 527 | q.update(out) | 622 | q.update(out) |
| 528 | 623 | ||
| 529 | - # Finally we create an instance of Question() | 624 | + # Get class for this question type |
| 530 | try: | 625 | try: |
| 531 | - qinstance = self._types[q['type']](QDict(q)) # of matching class | ||
| 532 | - except QuestionException as e: | ||
| 533 | - logger.error(e) | ||
| 534 | - raise e | 626 | + qclass = self._types[q['type']] |
| 535 | except KeyError: | 627 | except KeyError: |
| 536 | logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') | 628 | logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') |
| 537 | raise | 629 | raise |
| 538 | - else: | ||
| 539 | - return qinstance | 630 | + |
| 631 | + # Finally create an instance of Question() | ||
| 632 | + try: | ||
| 633 | + qinstance = qclass(QDict(q)) | ||
| 634 | + except QuestionException as e: | ||
| 635 | + # logger.error(e) | ||
| 636 | + raise e | ||
| 637 | + | ||
| 638 | + return qinstance | ||
| 540 | 639 | ||
| 541 | # ------------------------------------------------------------------------ | 640 | # ------------------------------------------------------------------------ |
| 542 | def generate(self) -> Question: | 641 | def generate(self) -> Question: |
aprendizations/serve.py
| @@ -19,6 +19,7 @@ from tornado.escape import to_unicode | @@ -19,6 +19,7 @@ from tornado.escape import to_unicode | ||
| 19 | from .tools import md_to_html | 19 | from .tools import md_to_html |
| 20 | from . import APP_NAME | 20 | from . import APP_NAME |
| 21 | 21 | ||
| 22 | + | ||
| 22 | # setup logger for this module | 23 | # setup logger for this module |
| 23 | logger = logging.getLogger(__name__) | 24 | logger = logging.getLogger(__name__) |
| 24 | 25 | ||
| @@ -294,10 +295,10 @@ class QuestionHandler(BaseHandler): | @@ -294,10 +295,10 @@ class QuestionHandler(BaseHandler): | ||
| 294 | 295 | ||
| 295 | # --- get question to render | 296 | # --- get question to render |
| 296 | @tornado.web.authenticated | 297 | @tornado.web.authenticated |
| 297 | - def get(self): | ||
| 298 | - logger.debug('[QuestionHandler.get]') | 298 | + async def get(self): |
| 299 | + logger.debug('[QuestionHandler]') | ||
| 299 | user = self.current_user | 300 | user = self.current_user |
| 300 | - q = self.learn.get_current_question(user) | 301 | + q = await self.learn.get_question(user) |
| 301 | 302 | ||
| 302 | if q is not None: | 303 | if q is not None: |
| 303 | qhtml = self.render_string(self.templates[q['type']], | 304 | qhtml = self.render_string(self.templates[q['type']], |
| @@ -326,15 +327,13 @@ class QuestionHandler(BaseHandler): | @@ -326,15 +327,13 @@ class QuestionHandler(BaseHandler): | ||
| 326 | # --- post answer, returns what to do next: shake, new_question, finished | 327 | # --- post answer, returns what to do next: shake, new_question, finished |
| 327 | @tornado.web.authenticated | 328 | @tornado.web.authenticated |
| 328 | async def post(self) -> None: | 329 | async def post(self) -> None: |
| 329 | - logger.debug('[QuestionHandler.post]') | ||
| 330 | user = self.current_user | 330 | user = self.current_user |
| 331 | answer = self.get_body_arguments('answer') # list | 331 | answer = self.get_body_arguments('answer') # list |
| 332 | - logger.debug(f'user = {user}, answer = {answer}') | 332 | + qid = self.get_body_arguments('qid')[0] |
| 333 | + logger.debug(f'[QuestionHandler] answer={answer}') | ||
| 333 | 334 | ||
| 334 | # --- check if browser opened different questions simultaneously | 335 | # --- check if browser opened different questions simultaneously |
| 335 | - answer_qid = self.get_body_arguments('qid')[0] | ||
| 336 | - current_qid = self.learn.get_current_question_id(user) | ||
| 337 | - if answer_qid != current_qid: | 336 | + if qid != self.learn.get_current_question_id(user): |
| 338 | logger.info(f'User {user} desynchronized questions') | 337 | logger.info(f'User {user} desynchronized questions') |
| 339 | self.write({ | 338 | self.write({ |
| 340 | 'method': 'invalid', | 339 | 'method': 'invalid', |
| @@ -346,7 +345,7 @@ class QuestionHandler(BaseHandler): | @@ -346,7 +345,7 @@ class QuestionHandler(BaseHandler): | ||
| 346 | return | 345 | return |
| 347 | 346 | ||
| 348 | # --- brain hacking ;) | 347 | # --- brain hacking ;) |
| 349 | - await asyncio.sleep(1) | 348 | + await asyncio.sleep(2) |
| 350 | 349 | ||
| 351 | # --- answers are in a list. fix depending on question type | 350 | # --- answers are in a list. fix depending on question type |
| 352 | qtype = self.learn.get_student_question_type(user) | 351 | qtype = self.learn.get_student_question_type(user) |
| @@ -361,11 +360,11 @@ class QuestionHandler(BaseHandler): | @@ -361,11 +360,11 @@ class QuestionHandler(BaseHandler): | ||
| 361 | ans = answer | 360 | ans = answer |
| 362 | 361 | ||
| 363 | # --- check answer (nonblocking) and get corrected question and action | 362 | # --- check answer (nonblocking) and get corrected question and action |
| 364 | - q, action = await self.learn.check_answer(user, ans) | 363 | + q = await self.learn.check_answer(user, ans) |
| 365 | 364 | ||
| 366 | # --- built response to return | 365 | # --- built response to return |
| 367 | - response = {'method': action, 'params': {}} | ||
| 368 | - if action == 'right': # get next question in the topic | 366 | + response = {'method': q['status'], 'params': {}} |
| 367 | + if q['status'] == 'right': # get next question in the topic | ||
| 369 | comments_html = self.render_string( | 368 | comments_html = self.render_string( |
| 370 | 'comments-right.html', comments=q['comments'], md=md_to_html) | 369 | 'comments-right.html', comments=q['comments'], md=md_to_html) |
| 371 | 370 | ||
| @@ -379,7 +378,7 @@ class QuestionHandler(BaseHandler): | @@ -379,7 +378,7 @@ class QuestionHandler(BaseHandler): | ||
| 379 | 'solution': to_unicode(solution_html), | 378 | 'solution': to_unicode(solution_html), |
| 380 | 'tries': q['tries'], | 379 | 'tries': q['tries'], |
| 381 | } | 380 | } |
| 382 | - elif action == 'try_again': | 381 | + elif q['status'] == 'try_again': |
| 383 | comments_html = self.render_string( | 382 | comments_html = self.render_string( |
| 384 | 'comments.html', comments=q['comments'], md=md_to_html) | 383 | 'comments.html', comments=q['comments'], md=md_to_html) |
| 385 | 384 | ||
| @@ -389,7 +388,7 @@ class QuestionHandler(BaseHandler): | @@ -389,7 +388,7 @@ class QuestionHandler(BaseHandler): | ||
| 389 | 'comments': to_unicode(comments_html), | 388 | 'comments': to_unicode(comments_html), |
| 390 | 'tries': q['tries'], | 389 | 'tries': q['tries'], |
| 391 | } | 390 | } |
| 392 | - elif action == 'wrong': # no more tries | 391 | + elif q['status'] == 'wrong': # no more tries |
| 393 | comments_html = self.render_string( | 392 | comments_html = self.render_string( |
| 394 | 'comments.html', comments=q['comments'], md=md_to_html) | 393 | 'comments.html', comments=q['comments'], md=md_to_html) |
| 395 | 394 | ||
| @@ -404,7 +403,7 @@ class QuestionHandler(BaseHandler): | @@ -404,7 +403,7 @@ class QuestionHandler(BaseHandler): | ||
| 404 | 'tries': q['tries'], | 403 | 'tries': q['tries'], |
| 405 | } | 404 | } |
| 406 | else: | 405 | else: |
| 407 | - logger.error(f'Unknown action: {action}') | 406 | + logger.error(f'Unknown question status: {q["status"]}') |
| 408 | 407 | ||
| 409 | self.write(response) | 408 | self.write(response) |
| 410 | 409 | ||
| @@ -457,7 +456,7 @@ def run_webserver(app, | @@ -457,7 +456,7 @@ def run_webserver(app, | ||
| 457 | 456 | ||
| 458 | # --- run webserver | 457 | # --- run webserver |
| 459 | signal.signal(signal.SIGINT, signal_handler) | 458 | signal.signal(signal.SIGINT, signal_handler) |
| 460 | - logger.info('Webserver running. (Ctrl-C to stop)') | 459 | + logger.info('Webserver running... (Ctrl-C to stop)') |
| 461 | 460 | ||
| 462 | try: | 461 | try: |
| 463 | tornado.ioloop.IOLoop.current().start() # running... | 462 | tornado.ioloop.IOLoop.current().start() # running... |
aprendizations/static/css/topic.css
aprendizations/student.py
| @@ -74,11 +74,11 @@ class StudentState(object): | @@ -74,11 +74,11 @@ class StudentState(object): | ||
| 74 | logger.debug(f'is locked "{topic}"') | 74 | logger.debug(f'is locked "{topic}"') |
| 75 | return | 75 | return |
| 76 | 76 | ||
| 77 | - # starting new topic | 77 | + # choose k questions |
| 78 | self.current_topic = topic | 78 | self.current_topic = topic |
| 79 | + # self.current_question = None | ||
| 79 | self.correct_answers = 0 | 80 | self.correct_answers = 0 |
| 80 | self.wrong_answers = 0 | 81 | self.wrong_answers = 0 |
| 81 | - | ||
| 82 | t = self.deps.nodes[topic] | 82 | t = self.deps.nodes[topic] |
| 83 | k = t['choose'] | 83 | k = t['choose'] |
| 84 | if t['shuffle_questions']: | 84 | if t['shuffle_questions']: |
| @@ -90,8 +90,7 @@ class StudentState(object): | @@ -90,8 +90,7 @@ class StudentState(object): | ||
| 90 | self.questions: List[Question] = [await self.factory[ref].gen_async() | 90 | self.questions: List[Question] = [await self.factory[ref].gen_async() |
| 91 | for ref in questions] | 91 | for ref in questions] |
| 92 | 92 | ||
| 93 | - n = len(self.questions) | ||
| 94 | - logger.debug(f'generated {n} questions') | 93 | + logger.debug(f'generated {len(self.questions)} questions') |
| 95 | 94 | ||
| 96 | # get first question | 95 | # get first question |
| 97 | self.next_question() | 96 | self.next_question() |
| @@ -110,62 +109,70 @@ class StudentState(object): | @@ -110,62 +109,70 @@ class StudentState(object): | ||
| 110 | self.wrong_answers) | 109 | self.wrong_answers) |
| 111 | } | 110 | } |
| 112 | self.current_topic = None | 111 | self.current_topic = None |
| 112 | + self.current_question = None | ||
| 113 | self.unlock_topics() | 113 | self.unlock_topics() |
| 114 | 114 | ||
| 115 | # ------------------------------------------------------------------------ | 115 | # ------------------------------------------------------------------------ |
| 116 | - # corrects current question with provided answer. | ||
| 117 | - # implements the logic: | ||
| 118 | - # - if answer ok, goes to next question | ||
| 119 | - # - if wrong, counts number of tries. If exceeded, moves on. | 116 | + # corrects current question |
| 120 | # ------------------------------------------------------------------------ | 117 | # ------------------------------------------------------------------------ |
| 121 | async def check_answer(self, answer) -> Tuple[Question, str]: | 118 | async def check_answer(self, answer) -> Tuple[Question, str]: |
| 122 | - q: Question = self.current_question | 119 | + q = self.current_question |
| 123 | q['answer'] = answer | 120 | q['answer'] = answer |
| 124 | q['finish_time'] = datetime.now() | 121 | q['finish_time'] = datetime.now() |
| 125 | - logger.debug(f'checking answer of {q["ref"]}...') | ||
| 126 | await q.correct_async() | 122 | await q.correct_async() |
| 127 | - logger.debug(f'grade = {q["grade"]:.2}') | ||
| 128 | 123 | ||
| 129 | if q['grade'] > 0.999: | 124 | if q['grade'] > 0.999: |
| 130 | self.correct_answers += 1 | 125 | self.correct_answers += 1 |
| 131 | - self.next_question() | ||
| 132 | - action = 'right' | 126 | + q['status'] = 'right' |
| 133 | 127 | ||
| 134 | else: | 128 | else: |
| 135 | self.wrong_answers += 1 | 129 | self.wrong_answers += 1 |
| 136 | - self.current_question['tries'] -= 1 | ||
| 137 | - | ||
| 138 | - if self.current_question['tries'] > 0: | ||
| 139 | - action = 'try_again' | 130 | + q['tries'] -= 1 |
| 131 | + if q['tries'] > 0: | ||
| 132 | + q['status'] = 'try_again' | ||
| 140 | else: | 133 | else: |
| 141 | - action = 'wrong' | ||
| 142 | - if self.current_question['append_wrong']: | ||
| 143 | - logger.debug('wrong answer, append new question') | ||
| 144 | - # self.questions.append(self.factory[q['ref']].generate()) | ||
| 145 | - new_question = await self.factory[q['ref']].gen_async() | ||
| 146 | - self.questions.append(new_question) | ||
| 147 | - self.next_question() | 134 | + q['status'] = 'wrong' |
| 148 | 135 | ||
| 149 | - # returns corrected question (not new one) which might include comments | ||
| 150 | - return q, action | 136 | + logger.debug(f'ref = {q["ref"]}, status = {q["status"]}') |
| 137 | + return q | ||
| 151 | 138 | ||
| 152 | # ------------------------------------------------------------------------ | 139 | # ------------------------------------------------------------------------ |
| 153 | - # Move to next question, or None | 140 | + # get question to show, current or next |
| 154 | # ------------------------------------------------------------------------ | 141 | # ------------------------------------------------------------------------ |
| 155 | - def next_question(self) -> Optional[Question]: | 142 | + async def get_question(self) -> Optional[Question]: |
| 143 | + q = self.current_question | ||
| 144 | + logger.debug(f'{q["ref"]} status = {q["status"]}') | ||
| 145 | + | ||
| 146 | + if q['status'] == 'right': | ||
| 147 | + self.next_question() | ||
| 148 | + elif q['status'] == 'wrong': | ||
| 149 | + if q['append_wrong']: | ||
| 150 | + logger.debug(' wrong answer => append new question') | ||
| 151 | + new_question = await self.factory[q['ref']].gen_async() | ||
| 152 | + self.questions.append(new_question) | ||
| 153 | + self.next_question() | ||
| 154 | + # elif q['status'] == 'new': | ||
| 155 | + # pass | ||
| 156 | + # elif q['status'] == 'try_again': | ||
| 157 | + # pass | ||
| 158 | + | ||
| 159 | + return self.current_question | ||
| 160 | + | ||
| 161 | + # ------------------------------------------------------------------------ | ||
| 162 | + # moves to next question | ||
| 163 | + # ------------------------------------------------------------------------ | ||
| 164 | + def next_question(self) -> None: | ||
| 156 | try: | 165 | try: |
| 157 | - self.current_question = self.questions.pop(0) | 166 | + q = self.questions.pop(0) |
| 158 | except IndexError: | 167 | except IndexError: |
| 159 | - self.current_question = None | ||
| 160 | self.finish_topic() | 168 | self.finish_topic() |
| 161 | - else: | ||
| 162 | - self.current_question['start_time'] = datetime.now() | ||
| 163 | - default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] | ||
| 164 | - maxtries = self.current_question.get('max_tries', default_maxtries) | ||
| 165 | - self.current_question['tries'] = maxtries | ||
| 166 | - logger.debug(f'current_question = {self.current_question["ref"]}') | 169 | + return |
| 167 | 170 | ||
| 168 | - return self.current_question # question or None | 171 | + t = self.deps.nodes[self.current_topic] |
| 172 | + q['start_time'] = datetime.now() | ||
| 173 | + q['tries'] = q.get('max_tries', t['max_tries']) | ||
| 174 | + q['status'] = 'new' | ||
| 175 | + self.current_question = q | ||
| 169 | 176 | ||
| 170 | # ------------------------------------------------------------------------ | 177 | # ------------------------------------------------------------------------ |
| 171 | # Update proficiency level of the topics using a forgetting factor | 178 | # Update proficiency level of the topics using a forgetting factor |
| @@ -178,7 +185,7 @@ class StudentState(object): | @@ -178,7 +185,7 @@ class StudentState(object): | ||
| 178 | forgetting_factor = self.deps.nodes[tref]['forgetting_factor'] | 185 | forgetting_factor = self.deps.nodes[tref]['forgetting_factor'] |
| 179 | s['level'] *= forgetting_factor ** dt.days # forgetting factor | 186 | s['level'] *= forgetting_factor ** dt.days # forgetting factor |
| 180 | except KeyError: | 187 | except KeyError: |
| 181 | - logger.warning(f'Topic {tref} is not on the graph!') | 188 | + logger.warning(f'Update topic levels: {tref} not in the graph') |
| 182 | 189 | ||
| 183 | # ------------------------------------------------------------------------ | 190 | # ------------------------------------------------------------------------ |
| 184 | # Unlock topics whose dependencies are satisfied (> min_level) | 191 | # Unlock topics whose dependencies are satisfied (> min_level) |
aprendizations/templates/question-checkbox.html
| @@ -9,7 +9,9 @@ | @@ -9,7 +9,9 @@ | ||
| 9 | <div class="custom-control custom-checkbox"> | 9 | <div class="custom-control custom-checkbox"> |
| 10 | <input type="checkbox" class="custom-control-input" | 10 | <input type="checkbox" class="custom-control-input" |
| 11 | id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}"> | 11 | id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}"> |
| 12 | - <label for="{{ n }}" class="custom-control-label">{{ md(opt, strip_p_tag=True) }}</label> | 12 | + <label for="{{ n }}" class="custom-control-label"> |
| 13 | + {{ md(opt, strip_p_tag=True) }} | ||
| 14 | + </label> | ||
| 13 | </div> | 15 | </div> |
| 14 | </a> | 16 | </a> |
| 15 | {% end %} | 17 | {% end %} |
aprendizations/templates/question-radio.html
| @@ -9,7 +9,9 @@ | @@ -9,7 +9,9 @@ | ||
| 9 | <div class="custom-control custom-radio"> | 9 | <div class="custom-control custom-radio"> |
| 10 | <input type="radio" class="custom-control-input" | 10 | <input type="radio" class="custom-control-input" |
| 11 | id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}"> | 11 | id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}"> |
| 12 | - <label for="{{ n }}" class="custom-control-label">{{ md(opt, strip_p_tag=True) }}</label> | 12 | + <label for="{{ n }}" class="custom-control-label"> |
| 13 | + {{ md(opt, strip_p_tag=True) }} | ||
| 14 | + </label> | ||
| 13 | </div> | 15 | </div> |
| 14 | </a> | 16 | </a> |
| 15 | {% end %} | 17 | {% end %} |
config/logger-debug.yaml
| @@ -2,50 +2,52 @@ | @@ -2,50 +2,52 @@ | ||
| 2 | version: 1 | 2 | version: 1 |
| 3 | 3 | ||
| 4 | formatters: | 4 | formatters: |
| 5 | - void: | ||
| 6 | - format: '' | ||
| 7 | - standard: | ||
| 8 | - format: '%(asctime)s | %(thread)-15d | %(levelname)-8s | %(module)-10s | %(funcName)-20s | %(message)s' | ||
| 9 | - # datefmt: '%H:%M:%S' | 5 | + void: |
| 6 | + format: '' | ||
| 7 | + standard: | ||
| 8 | + format: '%(asctime)s | %(levelname)-8s | %(module)-10s | %(funcName)-22s | | ||
| 9 | + %(message)s' | ||
| 10 | + # | %(thread)-15d | ||
| 11 | + # datefmt: '%H:%M:%S' | ||
| 10 | 12 | ||
| 11 | handlers: | 13 | handlers: |
| 12 | - default: | ||
| 13 | - level: 'DEBUG' | ||
| 14 | - class: 'logging.StreamHandler' | ||
| 15 | - formatter: 'standard' | ||
| 16 | - stream: 'ext://sys.stdout' | 14 | + default: |
| 15 | + level: 'DEBUG' | ||
| 16 | + class: 'logging.StreamHandler' | ||
| 17 | + formatter: 'standard' | ||
| 18 | + stream: 'ext://sys.stdout' | ||
| 17 | 19 | ||
| 18 | loggers: | 20 | loggers: |
| 19 | - '': | ||
| 20 | - handlers: ['default'] | ||
| 21 | - level: 'DEBUG' | ||
| 22 | - | ||
| 23 | - 'aprendizations.factory': | ||
| 24 | - handlers: ['default'] | ||
| 25 | - level: 'DEBUG' | ||
| 26 | - propagate: false | ||
| 27 | - | ||
| 28 | - 'aprendizations.student': | ||
| 29 | - handlers: ['default'] | ||
| 30 | - level: 'DEBUG' | ||
| 31 | - propagate: false | ||
| 32 | - | ||
| 33 | - 'aprendizations.learnapp': | ||
| 34 | - handlers: ['default'] | ||
| 35 | - level: 'DEBUG' | ||
| 36 | - propagate: false | ||
| 37 | - | ||
| 38 | - 'aprendizations.questions': | ||
| 39 | - handlers: ['default'] | ||
| 40 | - level: 'DEBUG' | ||
| 41 | - propagate: false | ||
| 42 | - | ||
| 43 | - 'aprendizations.tools': | ||
| 44 | - handlers: ['default'] | ||
| 45 | - level: 'DEBUG' | ||
| 46 | - propagate: false | ||
| 47 | - | ||
| 48 | - 'aprendizations.serve': | ||
| 49 | - handlers: ['default'] | ||
| 50 | - level: 'DEBUG' | ||
| 51 | - propagate: false | 21 | + '': |
| 22 | + handlers: ['default'] | ||
| 23 | + level: 'DEBUG' | ||
| 24 | + | ||
| 25 | + 'aprendizations.factory': | ||
| 26 | + handlers: ['default'] | ||
| 27 | + level: 'DEBUG' | ||
| 28 | + propagate: false | ||
| 29 | + | ||
| 30 | + 'aprendizations.student': | ||
| 31 | + handlers: ['default'] | ||
| 32 | + level: 'DEBUG' | ||
| 33 | + propagate: false | ||
| 34 | + | ||
| 35 | + 'aprendizations.learnapp': | ||
| 36 | + handlers: ['default'] | ||
| 37 | + level: 'DEBUG' | ||
| 38 | + propagate: false | ||
| 39 | + | ||
| 40 | + 'aprendizations.questions': | ||
| 41 | + handlers: ['default'] | ||
| 42 | + level: 'DEBUG' | ||
| 43 | + propagate: false | ||
| 44 | + | ||
| 45 | + 'aprendizations.tools': | ||
| 46 | + handlers: ['default'] | ||
| 47 | + level: 'DEBUG' | ||
| 48 | + propagate: false | ||
| 49 | + | ||
| 50 | + 'aprendizations.serve': | ||
| 51 | + handlers: ['default'] | ||
| 52 | + level: 'DEBUG' | ||
| 53 | + propagate: false |
demo/astronomy.yaml
| @@ -3,11 +3,11 @@ | @@ -3,11 +3,11 @@ | ||
| 3 | # optional values applied to each topic, if undefined there | 3 | # optional values applied to each topic, if undefined there |
| 4 | # ---------------------------------------------------------------------------- | 4 | # ---------------------------------------------------------------------------- |
| 5 | 5 | ||
| 6 | -# defaults: | 6 | +# defaults: FIXME not working |
| 7 | # file: questions.yaml | 7 | # file: questions.yaml |
| 8 | # shuffle_questions: true | 8 | # shuffle_questions: true |
| 9 | # choose: 6 | 9 | # choose: 6 |
| 10 | -# max_tries: 2 | 10 | +# max_tries: 200 |
| 11 | # forgetting_factor: 0.97 | 11 | # forgetting_factor: 0.97 |
| 12 | # min_level: 0.01 | 12 | # min_level: 0.01 |
| 13 | # append_wrong: true | 13 | # append_wrong: true |
demo/astronomy/solar-system/questions.yaml
| @@ -28,6 +28,7 @@ | @@ -28,6 +28,7 @@ | ||
| 28 | - Têm todos o mesmo tamanho | 28 | - Têm todos o mesmo tamanho |
| 29 | # opcional | 29 | # opcional |
| 30 | correct: 2 | 30 | correct: 2 |
| 31 | + # discount: true | ||
| 31 | shuffle: false | 32 | shuffle: false |
| 32 | solution: | | 33 | solution: | |
| 33 | O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas | 34 | O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas |
demo/math/addition/questions.yaml
| @@ -5,6 +5,7 @@ | @@ -5,6 +5,7 @@ | ||
| 5 | script: addition-two-digits.py | 5 | script: addition-two-digits.py |
| 6 | args: [10, 20] | 6 | args: [10, 20] |
| 7 | 7 | ||
| 8 | +# --------------------------------------------------------------------------- | ||
| 8 | - type: checkbox | 9 | - type: checkbox |
| 9 | ref: addition-properties | 10 | ref: addition-properties |
| 10 | title: Propriedades da adição | 11 | title: Propriedades da adição |
| @@ -17,6 +18,6 @@ | @@ -17,6 +18,6 @@ | ||
| 17 | - Propriedade comutativa, $x+y=y+x$. | 18 | - Propriedade comutativa, $x+y=y+x$. |
| 18 | # wrong | 19 | # wrong |
| 19 | - Existência de elemento absorvente, $x+1=1$. | 20 | - Existência de elemento absorvente, $x+1=1$. |
| 20 | - correct: [1, 1, 1, 1, -1] | 21 | + correct: [1, 1, 1, 1, 0] |
| 21 | solution: | | 22 | solution: | |
| 22 | A adição não tem elemento absorvente. | 23 | A adição não tem elemento absorvente. |
package-lock.json
| @@ -2,244 +2,26 @@ | @@ -2,244 +2,26 @@ | ||
| 2 | "requires": true, | 2 | "requires": true, |
| 3 | "lockfileVersion": 1, | 3 | "lockfileVersion": 1, |
| 4 | "dependencies": { | 4 | "dependencies": { |
| 5 | - "@babel/code-frame": { | ||
| 6 | - "version": "7.5.5", | ||
| 7 | - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", | ||
| 8 | - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", | ||
| 9 | - "requires": { | ||
| 10 | - "@babel/highlight": "^7.0.0" | ||
| 11 | - } | ||
| 12 | - }, | ||
| 13 | - "@babel/core": { | ||
| 14 | - "version": "7.6.0", | ||
| 15 | - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.6.0.tgz", | ||
| 16 | - "integrity": "sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw==", | ||
| 17 | - "requires": { | ||
| 18 | - "@babel/code-frame": "^7.5.5", | ||
| 19 | - "@babel/generator": "^7.6.0", | ||
| 20 | - "@babel/helpers": "^7.6.0", | ||
| 21 | - "@babel/parser": "^7.6.0", | ||
| 22 | - "@babel/template": "^7.6.0", | ||
| 23 | - "@babel/traverse": "^7.6.0", | ||
| 24 | - "@babel/types": "^7.6.0", | ||
| 25 | - "convert-source-map": "^1.1.0", | ||
| 26 | - "debug": "^4.1.0", | ||
| 27 | - "json5": "^2.1.0", | ||
| 28 | - "lodash": "^4.17.13", | ||
| 29 | - "resolve": "^1.3.2", | ||
| 30 | - "semver": "^5.4.1", | ||
| 31 | - "source-map": "^0.5.0" | ||
| 32 | - } | ||
| 33 | - }, | ||
| 34 | - "@babel/generator": { | ||
| 35 | - "version": "7.6.0", | ||
| 36 | - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz", | ||
| 37 | - "integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==", | ||
| 38 | - "requires": { | ||
| 39 | - "@babel/types": "^7.6.0", | ||
| 40 | - "jsesc": "^2.5.1", | ||
| 41 | - "lodash": "^4.17.13", | ||
| 42 | - "source-map": "^0.5.0", | ||
| 43 | - "trim-right": "^1.0.1" | ||
| 44 | - } | ||
| 45 | - }, | ||
| 46 | - "@babel/helper-function-name": { | ||
| 47 | - "version": "7.1.0", | ||
| 48 | - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", | ||
| 49 | - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", | ||
| 50 | - "requires": { | ||
| 51 | - "@babel/helper-get-function-arity": "^7.0.0", | ||
| 52 | - "@babel/template": "^7.1.0", | ||
| 53 | - "@babel/types": "^7.0.0" | ||
| 54 | - } | ||
| 55 | - }, | ||
| 56 | - "@babel/helper-get-function-arity": { | ||
| 57 | - "version": "7.0.0", | ||
| 58 | - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", | ||
| 59 | - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", | ||
| 60 | - "requires": { | ||
| 61 | - "@babel/types": "^7.0.0" | ||
| 62 | - } | ||
| 63 | - }, | ||
| 64 | - "@babel/helper-split-export-declaration": { | ||
| 65 | - "version": "7.4.4", | ||
| 66 | - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", | ||
| 67 | - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", | ||
| 68 | - "requires": { | ||
| 69 | - "@babel/types": "^7.4.4" | ||
| 70 | - } | ||
| 71 | - }, | ||
| 72 | - "@babel/helpers": { | ||
| 73 | - "version": "7.6.0", | ||
| 74 | - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.6.0.tgz", | ||
| 75 | - "integrity": "sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ==", | ||
| 76 | - "requires": { | ||
| 77 | - "@babel/template": "^7.6.0", | ||
| 78 | - "@babel/traverse": "^7.6.0", | ||
| 79 | - "@babel/types": "^7.6.0" | ||
| 80 | - } | ||
| 81 | - }, | ||
| 82 | - "@babel/highlight": { | ||
| 83 | - "version": "7.5.0", | ||
| 84 | - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", | ||
| 85 | - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", | ||
| 86 | - "requires": { | ||
| 87 | - "chalk": "^2.0.0", | ||
| 88 | - "esutils": "^2.0.2", | ||
| 89 | - "js-tokens": "^4.0.0" | ||
| 90 | - } | ||
| 91 | - }, | ||
| 92 | - "@babel/parser": { | ||
| 93 | - "version": "7.6.0", | ||
| 94 | - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", | ||
| 95 | - "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" | ||
| 96 | - }, | ||
| 97 | - "@babel/template": { | ||
| 98 | - "version": "7.6.0", | ||
| 99 | - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", | ||
| 100 | - "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", | ||
| 101 | - "requires": { | ||
| 102 | - "@babel/code-frame": "^7.0.0", | ||
| 103 | - "@babel/parser": "^7.6.0", | ||
| 104 | - "@babel/types": "^7.6.0" | ||
| 105 | - } | ||
| 106 | - }, | ||
| 107 | - "@babel/traverse": { | ||
| 108 | - "version": "7.6.0", | ||
| 109 | - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz", | ||
| 110 | - "integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==", | ||
| 111 | - "requires": { | ||
| 112 | - "@babel/code-frame": "^7.5.5", | ||
| 113 | - "@babel/generator": "^7.6.0", | ||
| 114 | - "@babel/helper-function-name": "^7.1.0", | ||
| 115 | - "@babel/helper-split-export-declaration": "^7.4.4", | ||
| 116 | - "@babel/parser": "^7.6.0", | ||
| 117 | - "@babel/types": "^7.6.0", | ||
| 118 | - "debug": "^4.1.0", | ||
| 119 | - "globals": "^11.1.0", | ||
| 120 | - "lodash": "^4.17.13" | ||
| 121 | - } | ||
| 122 | - }, | ||
| 123 | - "@babel/types": { | ||
| 124 | - "version": "7.6.1", | ||
| 125 | - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", | ||
| 126 | - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", | ||
| 127 | - "requires": { | ||
| 128 | - "esutils": "^2.0.2", | ||
| 129 | - "lodash": "^4.17.13", | ||
| 130 | - "to-fast-properties": "^2.0.0" | ||
| 131 | - } | ||
| 132 | - }, | ||
| 133 | "@fortawesome/fontawesome-free": { | 5 | "@fortawesome/fontawesome-free": { |
| 134 | "version": "5.11.2", | 6 | "version": "5.11.2", |
| 135 | "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz", | 7 | "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz", |
| 136 | "integrity": "sha512-XiUPoS79r1G7PcpnNtq85TJ7inJWe0v+b5oZJZKb0pGHNIV6+UiNeQWiFGmuQ0aj7GEhnD/v9iqxIsjuRKtEnQ==" | 8 | "integrity": "sha512-XiUPoS79r1G7PcpnNtq85TJ7inJWe0v+b5oZJZKb0pGHNIV6+UiNeQWiFGmuQ0aj7GEhnD/v9iqxIsjuRKtEnQ==" |
| 137 | }, | 9 | }, |
| 138 | - "ansi-styles": { | ||
| 139 | - "version": "3.2.1", | ||
| 140 | - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", | ||
| 141 | - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", | ||
| 142 | - "requires": { | ||
| 143 | - "color-convert": "^1.9.0" | ||
| 144 | - } | ||
| 145 | - }, | ||
| 146 | - "chalk": { | ||
| 147 | - "version": "2.4.2", | ||
| 148 | - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", | ||
| 149 | - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", | ||
| 150 | - "requires": { | ||
| 151 | - "ansi-styles": "^3.2.1", | ||
| 152 | - "escape-string-regexp": "^1.0.5", | ||
| 153 | - "supports-color": "^5.3.0" | ||
| 154 | - } | ||
| 155 | - }, | ||
| 156 | "codemirror": { | 10 | "codemirror": { |
| 157 | - "version": "5.49.0", | ||
| 158 | - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.0.tgz", | ||
| 159 | - "integrity": "sha512-Hyzr0HToBdZpLBN9dYFO/KlJAsKH37/cXVHPAqa+imml0R92tb9AkmsvjnXL+SluEvjjdfkDgRjc65NG5jnMYA==" | ||
| 160 | - }, | ||
| 161 | - "color-convert": { | ||
| 162 | - "version": "1.9.3", | ||
| 163 | - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", | ||
| 164 | - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", | ||
| 165 | - "requires": { | ||
| 166 | - "color-name": "1.1.3" | ||
| 167 | - } | ||
| 168 | - }, | ||
| 169 | - "color-name": { | ||
| 170 | - "version": "1.1.3", | ||
| 171 | - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", | ||
| 172 | - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" | 11 | + "version": "5.49.2", |
| 12 | + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.2.tgz", | ||
| 13 | + "integrity": "sha512-dwJ2HRPHm8w51WB5YTF9J7m6Z5dtkqbU9ntMZ1dqXyFB9IpjoUFDj80ahRVEoVanfIp6pfASJbOlbWdEf8FOzQ==" | ||
| 173 | }, | 14 | }, |
| 174 | "commander": { | 15 | "commander": { |
| 175 | "version": "3.0.1", | 16 | "version": "3.0.1", |
| 176 | "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.1.tgz", | 17 | "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.1.tgz", |
| 177 | "integrity": "sha512-UNgvDd+csKdc9GD4zjtkHKQbT8Aspt2jCBqNSPp53vAS0L1tS9sXB2TCEOPHJ7kt9bN/niWkYj8T3RQSoMXdSQ==" | 18 | "integrity": "sha512-UNgvDd+csKdc9GD4zjtkHKQbT8Aspt2jCBqNSPp53vAS0L1tS9sXB2TCEOPHJ7kt9bN/niWkYj8T3RQSoMXdSQ==" |
| 178 | }, | 19 | }, |
| 179 | - "convert-source-map": { | ||
| 180 | - "version": "1.6.0", | ||
| 181 | - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", | ||
| 182 | - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", | ||
| 183 | - "requires": { | ||
| 184 | - "safe-buffer": "~5.1.1" | ||
| 185 | - } | ||
| 186 | - }, | ||
| 187 | - "debug": { | ||
| 188 | - "version": "4.1.1", | ||
| 189 | - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", | ||
| 190 | - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", | ||
| 191 | - "requires": { | ||
| 192 | - "ms": "^2.1.1" | ||
| 193 | - } | ||
| 194 | - }, | ||
| 195 | - "escape-string-regexp": { | ||
| 196 | - "version": "1.0.5", | ||
| 197 | - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", | ||
| 198 | - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" | ||
| 199 | - }, | ||
| 200 | "esm": { | 20 | "esm": { |
| 201 | "version": "3.2.25", | 21 | "version": "3.2.25", |
| 202 | "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", | 22 | "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", |
| 203 | "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" | 23 | "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" |
| 204 | }, | 24 | }, |
| 205 | - "esutils": { | ||
| 206 | - "version": "2.0.3", | ||
| 207 | - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", | ||
| 208 | - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" | ||
| 209 | - }, | ||
| 210 | - "globals": { | ||
| 211 | - "version": "11.12.0", | ||
| 212 | - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", | ||
| 213 | - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" | ||
| 214 | - }, | ||
| 215 | - "has-flag": { | ||
| 216 | - "version": "3.0.0", | ||
| 217 | - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", | ||
| 218 | - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" | ||
| 219 | - }, | ||
| 220 | - "js-tokens": { | ||
| 221 | - "version": "4.0.0", | ||
| 222 | - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", | ||
| 223 | - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" | ||
| 224 | - }, | ||
| 225 | - "jsesc": { | ||
| 226 | - "version": "2.5.2", | ||
| 227 | - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", | ||
| 228 | - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" | ||
| 229 | - }, | ||
| 230 | - "json5": { | ||
| 231 | - "version": "2.1.0", | ||
| 232 | - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", | ||
| 233 | - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", | ||
| 234 | - "requires": { | ||
| 235 | - "minimist": "^1.2.0" | ||
| 236 | - } | ||
| 237 | - }, | ||
| 238 | - "lodash": { | ||
| 239 | - "version": "4.17.15", | ||
| 240 | - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", | ||
| 241 | - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" | ||
| 242 | - }, | ||
| 243 | "mathjax": { | 25 | "mathjax": { |
| 244 | "version": "3.0.0", | 26 | "version": "3.0.0", |
| 245 | "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.0.0.tgz", | 27 | "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.0.0.tgz", |
| @@ -258,56 +40,15 @@ | @@ -258,56 +40,15 @@ | ||
| 258 | } | 40 | } |
| 259 | }, | 41 | }, |
| 260 | "mdbootstrap": { | 42 | "mdbootstrap": { |
| 261 | - "version": "4.8.10", | ||
| 262 | - "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.8.10.tgz", | ||
| 263 | - "integrity": "sha512-pUjs7Vds4J+MwepOo4obUy7bQ5aMeB8j1c3IxIcEYXOXmn8GOWMSpiRcfSXpH9R4Fgdfie++e0fm5+SebRnTYA==", | ||
| 264 | - "requires": { | ||
| 265 | - "@babel/core": "^7.3.3" | ||
| 266 | - } | ||
| 267 | - }, | ||
| 268 | - "minimist": { | ||
| 269 | - "version": "1.2.0", | ||
| 270 | - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", | ||
| 271 | - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" | 43 | + "version": "4.9.0", |
| 44 | + "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.9.0.tgz", | ||
| 45 | + "integrity": "sha512-6R3j5D9Qmp+Aa90FblOVAwVDSqpAICYW2dpNxh6uaVB9E9MCaBLdaTKLrXCB7xznReHEaA57pNABXgFoi2z7Rg==" | ||
| 272 | }, | 46 | }, |
| 273 | "mj-context-menu": { | 47 | "mj-context-menu": { |
| 274 | "version": "0.2.0", | 48 | "version": "0.2.0", |
| 275 | "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.2.0.tgz", | 49 | "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.2.0.tgz", |
| 276 | "integrity": "sha512-yJxrWBHCjFZEHsZgfs7m5g9OSCNzsVYadW6f6lX3pgZL67vmodtSW/4zhsYmuDKweXfHs0M1kJge1uQIasWA+g==" | 50 | "integrity": "sha512-yJxrWBHCjFZEHsZgfs7m5g9OSCNzsVYadW6f6lX3pgZL67vmodtSW/4zhsYmuDKweXfHs0M1kJge1uQIasWA+g==" |
| 277 | }, | 51 | }, |
| 278 | - "ms": { | ||
| 279 | - "version": "2.1.2", | ||
| 280 | - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", | ||
| 281 | - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" | ||
| 282 | - }, | ||
| 283 | - "path-parse": { | ||
| 284 | - "version": "1.0.6", | ||
| 285 | - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", | ||
| 286 | - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" | ||
| 287 | - }, | ||
| 288 | - "resolve": { | ||
| 289 | - "version": "1.12.0", | ||
| 290 | - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", | ||
| 291 | - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", | ||
| 292 | - "requires": { | ||
| 293 | - "path-parse": "^1.0.6" | ||
| 294 | - } | ||
| 295 | - }, | ||
| 296 | - "safe-buffer": { | ||
| 297 | - "version": "5.1.2", | ||
| 298 | - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", | ||
| 299 | - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" | ||
| 300 | - }, | ||
| 301 | - "semver": { | ||
| 302 | - "version": "5.7.1", | ||
| 303 | - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", | ||
| 304 | - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" | ||
| 305 | - }, | ||
| 306 | - "source-map": { | ||
| 307 | - "version": "0.5.7", | ||
| 308 | - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", | ||
| 309 | - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" | ||
| 310 | - }, | ||
| 311 | "speech-rule-engine": { | 52 | "speech-rule-engine": { |
| 312 | "version": "3.0.0-beta.6", | 53 | "version": "3.0.0-beta.6", |
| 313 | "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.0.0-beta.6.tgz", | 54 | "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.0.0-beta.6.tgz", |
| @@ -318,24 +59,6 @@ | @@ -318,24 +59,6 @@ | ||
| 318 | "xmldom-sre": "^0.1.31" | 59 | "xmldom-sre": "^0.1.31" |
| 319 | } | 60 | } |
| 320 | }, | 61 | }, |
| 321 | - "supports-color": { | ||
| 322 | - "version": "5.5.0", | ||
| 323 | - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", | ||
| 324 | - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", | ||
| 325 | - "requires": { | ||
| 326 | - "has-flag": "^3.0.0" | ||
| 327 | - } | ||
| 328 | - }, | ||
| 329 | - "to-fast-properties": { | ||
| 330 | - "version": "2.0.0", | ||
| 331 | - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", | ||
| 332 | - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" | ||
| 333 | - }, | ||
| 334 | - "trim-right": { | ||
| 335 | - "version": "1.0.1", | ||
| 336 | - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", | ||
| 337 | - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" | ||
| 338 | - }, | ||
| 339 | "wicked-good-xpath": { | 62 | "wicked-good-xpath": { |
| 340 | "version": "1.3.0", | 63 | "version": "1.3.0", |
| 341 | "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", | 64 | "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", |
package.json
| @@ -3,9 +3,9 @@ | @@ -3,9 +3,9 @@ | ||
| 3 | "email": "mjsb@uevora.pt", | 3 | "email": "mjsb@uevora.pt", |
| 4 | "dependencies": { | 4 | "dependencies": { |
| 5 | "@fortawesome/fontawesome-free": "^5.11.2", | 5 | "@fortawesome/fontawesome-free": "^5.11.2", |
| 6 | - "codemirror": "^5.49.0", | 6 | + "codemirror": "^5.49.2", |
| 7 | "mathjax": "^3", | 7 | "mathjax": "^3", |
| 8 | - "mdbootstrap": "^4.8.10" | 8 | + "mdbootstrap": "^4.9.0" |
| 9 | }, | 9 | }, |
| 10 | "private": true | 10 | "private": true |
| 11 | } | 11 | } |