From d823a4d87455beadabebfd26c488199af065fcea Mon Sep 17 00:00:00 2001 From: Miguel Barão Date: Sat, 23 Nov 2019 17:08:26 +0000 Subject: [PATCH] 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 --- BUGS.md | 58 +++++++++++++++------------------------------------------- aprendizations/learnapp.py | 67 ++++++++++++++++++++++++++++++++++++++++++++----------------------- aprendizations/main.py | 11 +++++++---- aprendizations/questions.py | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------- aprendizations/serve.py | 31 +++++++++++++++---------------- aprendizations/static/css/topic.css | 3 +++ aprendizations/student.py | 83 +++++++++++++++++++++++++++++++++++++++++++++-------------------------------------- aprendizations/templates/question-checkbox.html | 4 +++- aprendizations/templates/question-radio.html | 4 +++- config/logger-debug.yaml | 88 +++++++++++++++++++++++++++++++++++++++++++++------------------------------------------- demo/astronomy.yaml | 4 ++-- demo/astronomy/solar-system/questions.yaml | 1 + demo/math/addition/questions.yaml | 3 ++- package-lock.json | 289 ++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- package.json | 4 ++-- 15 files changed, 342 insertions(+), 507 deletions(-) diff --git a/BUGS.md b/BUGS.md index b22d01f..81b6095 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,59 +1,22 @@ # BUGS - -Traceback (most recent call last): - File "/home/mjsb/.local/lib/python3.7/site-packages/tornado/web.py", line 1697, in _execute - result = method(*self.path_args, **self.path_kwargs) - File "/home/mjsb/.local/lib/python3.7/site-packages/tornado/web.py", line 3174, in wrapper - return method(self, *args, **kwargs) - File "/usr/home/mjsb/Work/Projects/aprendizations/aprendizations/serve.py", line 213, in get - self.learn.start_course(uid, course) - File "/usr/home/mjsb/Work/Projects/aprendizations/aprendizations/learnapp.py", line 275, in start_course - student.start_course(course) - File "/usr/home/mjsb/Work/Projects/aprendizations/aprendizations/student.py", line 57, in start_course - self.topic_sequence = self.recommend_topic_sequence(topics) - File "/usr/home/mjsb/Work/Projects/aprendizations/aprendizations/student.py", line 216, in recommend_topic_sequence - ts.update(nx.ancestors(G, t)) - File "/home/mjsb/.local/lib/python3.7/site-packages/networkx/algorithms/dag.py", line 92, in ancestors - raise nx.NetworkXError("The node %s is not in the graph." % source) -networkx.exception.NetworkXError: The node programming/languages/pseudo-tcg/functions-produtorio is not in the graph. - - - -- detectar se em courses.yaml falta declarar ficheiro. Por exemplo se houver goals que não estao em lado nenhum. -- 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. -- registar last_seen e remover os antigos de cada vez que houver um login. +- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) +- nao esta a seguir o max_tries definido no ficheiro de dependencias. - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) -- 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. -- 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. -- marking all options right in a radio question breaks! -- opcao --prefix devia afectar a base de dados? - 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 -- quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo) -- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) - devia mostrar timeout para o aluno saber a razao. - permitir configuracao para escolher entre static files locais ou remotos -- sqlalchemy.pool.impl.NullPool: Exception during reset or similar -sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. - templates question-*.html tem input hidden question_ref que não é usado. remover? -- guardar o estado a meio de um nível. -- 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. -- click numa opcao checkbox fora da checkbox+label não está a funcionar. - shift-enter não está a funcionar -- 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. - default prefix should be obtained from each course (yaml conf)? -- tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. # TODO +- registar last_seen e remover os antigos de cada vez que houver um login. - indicar qtos topicos faltam (>=50%) para terminar o curso. -- use run_script_async to run run_script using asyncio.run? -- ao fim de 3 tentativas de login, envial email para aluno com link para definir nova password (com timeout de 5 minutos). +- ao fim de 3 tentativas com password errada, envia email com nova password. - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias. - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos. - botão não sei... @@ -65,14 +28,23 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in - tabela com perguntas / quantidade de respostas certas/erradas. - tabela com topicos / quantidade de estrelas. - pymips: activar/desactivar instruções -- implementar servidor http com redirect para https. -- ao fim de 3 tentativas com password errada, envia email com nova password. - titulos das perguntas não suportam markdown. - pagina report que permita ver tabela alunos/topicos, estatisticas perguntas mais falhadas, tempo médio por pergunta. - normalizar com perguntations. # FIXED +- 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. +- marking all options right in a radio question breaks! +- implementar servidor http com redirect para https. +- tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. +- click numa opcao checkbox fora da checkbox+label não está a funcionar. +- 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. +- QFactory.generate() devia fazer run da gen_async, ou remover. +- classificacoes so devia mostrar os que ja fizeram alguma coisa +- impedir que quando students.db não é encontrado, crie um ficheiro vazio. +- permite definir goal, mas nao verifica se esta no grafo. rebenta no start_topic. +- 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. - caixas com os cursos não se ajustam bem com ecran estreito. - obter rankings por curso GET course=course_id - no curso de linear algebra, as perguntas estao shuffled, mas nao deviam estar... nao esta a obedecer a keyword shuffle. diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index a72fcd4..9d410d3 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -16,10 +16,11 @@ import sqlalchemy as sa # this project from .models import Student, Answer, Topic, StudentTopic -from .questions import Question, QFactory, QDict +from .questions import Question, QFactory, QDict, QuestionException from .student import StudentState from .tools import load_yaml + # setup logger for this module logger = logging.getLogger(__name__) @@ -68,28 +69,36 @@ class LearnApp(object): config: Dict[str, Any] = load_yaml(courses) - # --- courses dict - self.courses = config['courses'] - logger.info(f'Courses: {", ".join(self.courses.keys())}') - # --- topic dependencies are shared between all courses self.deps = nx.DiGraph(prefix=prefix) - logger.info('Populating graph:') + logger.info('Populating topic graph:') 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}') + + # FIXME set defaults?? + logger.info(f'{len(c["topics"]):>6} topics imported from {f}') self.populate_graph(c) logger.info(f'Graph has {len(self.deps)} topics') + # --- courses dict + self.courses = config['courses'] + logger.info(f'Courses: {", ".join(self.courses.keys())}') + for c, d in self.courses.items(): + for goal in d['goals']: + if goal not in self.deps.nodes(): + # logger.error(f'Goal "{goal}" of "{c}"" not in the graph') + raise LearnException(f'Goal "{goal}" from course "{c}" ' + ' does not exist') + # --- 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 - self.db_add_missing_topics(self.deps.nodes()) + self.add_missing_topics(self.deps.nodes()) if check: self.sanity_check_questions() @@ -103,8 +112,8 @@ class LearnApp(object): logger.debug(f'checking {qref}...') try: q = self.factory[qref].generate() - except Exception: - logger.error(f'Failed to generate "{qref}".') + except QuestionException as e: + logger.error(e) errors += 1 continue # to next question @@ -127,7 +136,7 @@ class LearnApp(object): continue # to next test if errors > 0: - logger.error(f'{errors:>6} errors found.') + logger.error(f'{errors:>6} error(s) found.') raise LearnException('Sanity checks') else: logger.info(' 0 errors found.') @@ -216,13 +225,13 @@ class LearnApp(object): return True # ------------------------------------------------------------------------ - # checks answer (updating student state) and returns grade. FIXME type of answer + # Checks answer and update database. Returns corrected question. # ------------------------------------------------------------------------ - async def check_answer(self, uid: str, answer) -> Tuple[Question, str]: + async def check_answer(self, uid: str, answer) -> Question: student = self.online[uid]['state'] topic = student.get_current_topic() - q, action = await student.check_answer(answer) # may move questions + q = await student.check_answer(answer) logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') @@ -237,8 +246,8 @@ class LearnApp(object): topic_id=topic)) logger.debug(f'db insert answer of {q["ref"]}') + # save topic if finished if student.topic_has_finished(): - # finished topic, save into database logger.info(f'User "{uid}" finished "{topic}"') level: float = student.get_topic_level(topic) date: str = str(student.get_topic_date(topic)) @@ -264,7 +273,13 @@ class LearnApp(object): s.add(a) - return q, action + return q + + # ------------------------------------------------------------------------ + # get the question to show (current or new one) + # ------------------------------------------------------------------------ + async def get_question(self, uid: str) -> Optional[Question]: + return await self.online[uid]['state'].get_question() # ------------------------------------------------------------------------ # Start course @@ -277,7 +292,7 @@ class LearnApp(object): logger.warning(f'"{uid}" could not start course "{course}": {e}') raise else: - logger.info(f'"{uid}" started course "{course}"') + logger.info(f'User "{uid}" started course "{course}"') # ------------------------------------------------------------------------ # Start new topic @@ -294,7 +309,7 @@ class LearnApp(object): # ------------------------------------------------------------------------ # Fill db table 'Topic' with topics from the graph if not already there. # ------------------------------------------------------------------------ - def db_add_missing_topics(self, topics: List[str]) -> None: + def add_missing_topics(self, topics: List[str]) -> None: with self.db_session() as s: new_topics = [Topic(id=t) for t in topics if (t,) not in s.query(Topic.id)] @@ -310,6 +325,10 @@ class LearnApp(object): def db_setup(self, db: str) -> None: logger.info(f'Checking database "{db}":') + if not path.exists(db): + raise LearnException('Database does not exist. ' + 'Use "initdb-aprendizations" to create') + engine = sa.create_engine(f'sqlite:///{db}', echo=False) self.Session = sa.orm.sessionmaker(bind=engine) try: @@ -325,7 +344,7 @@ class LearnApp(object): logger.info(f'{m:6} topics') logger.info(f'{q:6} answers') - # ============================================================================ + # ======================================================================== # Populates a digraph. # # Nodes are the topic references e.g. 'my/topic' @@ -380,7 +399,7 @@ class LearnApp(object): def make_factory(self) -> Dict[str, QFactory]: logger.info('Building questions factory:') - factory: Dict[str, QFactory] = {} + factory = {} g = self.deps for tref in g.nodes(): t = g.nodes[tref] @@ -389,6 +408,7 @@ class LearnApp(object): topicpath: str = path.join(g.graph['prefix'], tref) fullpath: str = path.join(topicpath, t['file']) + logger.debug(f' Loading {fullpath}') questions: List[QDict] = load_yaml(fullpath, default=[]) # update refs to include topic as prefix. @@ -397,7 +417,7 @@ class LearnApp(object): # within the file for i, q in enumerate(questions): qref = q.get('ref', str(i)) # ref or number - q['ref'] = tref + ':' + qref + q['ref'] = f'{tref}:{qref}' q['path'] = topicpath q.setdefault('append_wrong', t['append_wrong']) @@ -410,10 +430,11 @@ class LearnApp(object): for q in questions: if q['ref'] in t['questions']: factory[q['ref']] = QFactory(q) + logger.debug(f' + {q["ref"]}') - logger.info(f'{len(t["questions"]):6} {tref}') + logger.info(f'{len(t["questions"]):6} questions in {tref}') - logger.info(f'Factory contains {len(factory)} questions') + logger.info(f'Factory has {len(factory)} questions') return factory # ------------------------------------------------------------------------ diff --git a/aprendizations/main.py b/aprendizations/main.py index 857b3e7..294168a 100644 --- a/aprendizations/main.py +++ b/aprendizations/main.py @@ -9,7 +9,7 @@ import sys from typing import Any, Dict # this project -from .learnapp import LearnApp, DatabaseUnusableError +from .learnapp import LearnApp, DatabaseUnusableError, LearnException from .serve import run_webserver from .tools import load_yaml from . import APP_NAME, APP_VERSION @@ -160,7 +160,7 @@ def main(): else: logging.info('SSL certificates loaded') - # --- start application + # --- start application -------------------------------------------------- try: learnapp = LearnApp(courses=arg.courses, prefix=arg.prefix, @@ -178,13 +178,16 @@ def main(): '--------------------------------------------------------------', sep='\n') sys.exit(1) + except LearnException as e: + logging.critical(e) + sys.exit(1) except Exception: logging.critical('Failed to start backend.') sys.exit(1) else: - logging.info('Backend started') + logging.info('LearnApp started') - # --- run webserver forever + # --- run webserver forever ---------------------------------------------- run_webserver(app=learnapp, ssl=ssl_ctx, port=arg.port, debug=arg.debug) diff --git a/aprendizations/questions.py b/aprendizations/questions.py index 82bc440..62fc9d3 100644 --- a/aprendizations/questions.py +++ b/aprendizations/questions.py @@ -42,7 +42,6 @@ class Question(dict): 'comments': '', 'solution': '', 'files': {}, - # 'max_tries': 3, })) def correct(self) -> None: @@ -72,7 +71,6 @@ class QuestionRadio(Question): ''' # ------------------------------------------------------------------------ - # FIXME marking all options right breaks def __init__(self, q: QDict) -> None: super().__init__(q) @@ -86,18 +84,46 @@ class QuestionRadio(Question): 'max_tries': (n + 3) // 4 # 1 try for each 4 options })) - # convert int to list, e.g. correct: 2 --> correct: [0,0,1,0,0] - # correctness levels from 0.0 to 1.0 (no discount here!) + # check correct bounds and convert int to list, + # e.g. correct: 2 --> correct: [0,0,1,0,0] if isinstance(self['correct'], int): + if not (0 <= self['correct'] < n): + msg = (f'Correct option not in range 0..{n-1} in ' + f'"{self["ref"]}"') + raise QuestionException(msg) + self['correct'] = [1.0 if x == self['correct'] else 0.0 for x in range(n)] - if len(self['correct']) != n: - msg = ('Number of options and correct differ in ' - f'"{self["ref"]}", file "{self["filename"]}".') - logger.error(msg) - raise QuestionException(msg) - + elif isinstance(self['correct'], list): + # must match number of options + if len(self['correct']) != n: + msg = (f'Incompatible sizes: {n} options vs ' + f'{len(self["correct"])} correct in "{self["ref"]}"') + raise QuestionException(msg) + # make sure is a list of floats + try: + self['correct'] = [float(x) for x in self['correct']] + except (ValueError, TypeError): + msg = (f'Correct list must contain numbers [0.0, 1.0] or ' + f'booleans in "{self["ref"]}"') + raise QuestionException(msg) + + # check grade boundaries + if self['discount'] and not all(0.0 <= x <= 1.0 + for x in self['correct']): + msg = (f'Correct values must be in the interval [0.0, 1.0] in ' + f'"{self["ref"]}"') + raise QuestionException(msg) + + # at least one correct option + if all(x < 1.0 for x in self['correct']): + msg = (f'At least one correct option is required in ' + f'"{self["ref"]}"') + raise QuestionException(msg) + + # If shuffle==false, all options are shown as defined + # otherwise, select 1 correct and choose a few wrong ones if self['shuffle']: # lists with indices of right and wrong options right = [i for i in range(n) if self['correct'][i] >= 1] @@ -123,7 +149,7 @@ class QuestionRadio(Question): # final shuffle of the options perm = random.sample(range(self['choose']), k=self['choose']) self['options'] = [str(options[i]) for i in perm] - self['correct'] = [float(correct[i]) for i in perm] + self['correct'] = [correct[i] for i in perm] # ------------------------------------------------------------------------ # can assign negative grades for wrong answers @@ -131,10 +157,13 @@ class QuestionRadio(Question): super().correct() if self['answer'] is not None: - x = self['correct'][int(self['answer'])] - if self['discount']: - n = len(self['options']) # number of options - x_aver = sum(self['correct']) / n + x = self['correct'][int(self['answer'])] # get grade of the answer + n = len(self['options']) + x_aver = sum(self['correct']) / n # expected value of grade + + # note: there are no numerical errors when summing 1.0s so the + # x_aver can be exactly 1.0 if all options are right + if self['discount'] and x_aver != 1.0: x = (x - x_aver) / (1.0 - x_aver) self['grade'] = x @@ -168,12 +197,44 @@ class QuestionCheckbox(Question): 'max_tries': max(1, min(n - 1, 3)) })) + # must be a list of numbers + if not isinstance(self['correct'], list): + msg = 'Correct must be a list of numbers or booleans' + raise QuestionException(msg) + + # must match number of options if len(self['correct']) != n: - msg = (f'Options and correct size mismatch in ' - f'"{self["ref"]}", file "{self["filename"]}".') - logger.error(msg) + msg = (f'Incompatible sizes: {n} options vs ' + f'{len(self["correct"])} correct in "{self["ref"]}"') + raise QuestionException(msg) + + # make sure is a list of floats + try: + self['correct'] = [float(x) for x in self['correct']] + except (ValueError, TypeError): + msg = (f'Correct list must contain numbers or ' + f'booleans in "{self["ref"]}"') raise QuestionException(msg) + # check grade boundaries + if self['discount'] and not all(0.0 <= x <= 1.0 + for x in self['correct']): + + msg0 = ('+-------------- BEHAVIOR CHANGE NOTICE --------------+') + msg1 = ('| Correct values must be in the interval [0.0, 1.0]. |') + msg2 = ('| I will convert to the new behavior, but you should |') + msg3 = ('| fix it in the question. |') + msg4 = ('+----------------------------------------------------+') + logger.warning(msg0) + logger.warning(msg1) + logger.warning(msg2) + logger.warning(msg3) + logger.warning(msg4) + logger.warning(f'please fix "{self["ref"]}"') + + # normalize to [0,1] + self['correct'] = [(x+1)/2 for x in self['correct']] + # if an option is a list of (right, wrong), pick one options = [] correct = [] @@ -182,9 +243,10 @@ class QuestionCheckbox(Question): r = random.randint(0, 1) o = o[r] if r == 1: - c = -c + # c = -c + c = 1.0 - c options.append(str(o)) - correct.append(float(c)) + correct.append(c) # generate random permutation, e.g. [2,1,4,0,3] # and apply to `options` and `correct` @@ -202,21 +264,20 @@ class QuestionCheckbox(Question): super().correct() if self['answer'] is not None: - sum_abs = sum(abs(p) for p in self['correct']) - if sum_abs < 1e-6: # case correct [0,...,0] avoid div-by-zero - self['grade'] = 1.0 - + x = 0.0 + if self['discount']: + sum_abs = sum(abs(2*p-1) for p in self['correct']) + for i, p in enumerate(self['correct']): + x += 2*p-1 if str(i) in self['answer'] else 1-2*p else: - x = 0.0 - - if self['discount']: - for i, p in enumerate(self['correct']): - x += p if str(i) in self['answer'] else -p - else: - for i, p in enumerate(self['correct']): - x += p if str(i) in self['answer'] else 0.0 + sum_abs = sum(abs(p) for p in self['correct']) + for i, p in enumerate(self['correct']): + x += p if str(i) in self['answer'] else 0.0 + try: self['grade'] = x / sum_abs + except ZeroDivisionError: + self['grade'] = 1.0 # limit p->0 # ============================================================================ @@ -240,15 +301,21 @@ class QuestionText(Question): # make sure its always a list of possible correct answers if not isinstance(self['correct'], list): - self['correct'] = [self['correct']] + self['correct'] = [str(self['correct'])] + else: + # make sure all elements of the list are strings + self['correct'] = [str(a) for a in self['correct']] - # make sure all elements of the list are strings - self['correct'] = [str(a) for a in self['correct']] + for f in self['transform']: + if f not in ('remove_space', 'trim', 'normalize_space', 'lower', + 'upper'): + msg = (f'Unknown transform "{f}" in "{self["ref"]}"') + raise QuestionException(msg) - # make sure that the answers are invariant with respect to the filters + # check if answers are invariant with respect to the transforms if any(c != self.transform(c) for c in self['correct']): logger.warning(f'in "{self["ref"]}", correct answers are not ' - 'invariant wrt transformations') + 'invariant wrt transformations => never correct') # ------------------------------------------------------------------------ # apply optional filters to the answer @@ -302,8 +369,12 @@ class QuestionTextRegex(Question): if not isinstance(self['correct'], list): self['correct'] = [self['correct']] - # make sure all elements of the list are strings - self['correct'] = [str(a) for a in self['correct']] + # converts patterns to compiled versions + try: + self['correct'] = [re.compile(a) for a in self['correct']] + except Exception: + msg = f'Failed to compile regex in "{self["ref"]}"' + raise QuestionException(msg) # ------------------------------------------------------------------------ def correct(self) -> None: @@ -312,12 +383,12 @@ class QuestionTextRegex(Question): self['grade'] = 0.0 for r in self['correct']: try: - if re.match(r, self['answer']): + if r.match(self['answer']): self['grade'] = 1.0 return except TypeError: - logger.error(f'While matching regex {self["correct"]} with' - f' answer "{self["answer"]}".') + logger.error(f'While matching regex {r.pattern} with ' + f'answer "{self["answer"]}".') # ============================================================================ @@ -339,6 +410,30 @@ class QuestionNumericInterval(Question): 'correct': [1.0, -1.0], # will always return false })) + # if only one number n is given, make an interval [n,n] + if isinstance(self['correct'], (int, float)): + self['correct'] = [float(self['correct']), float(self['correct'])] + + # make sure its a list of two numbers + elif isinstance(self['correct'], list): + if len(self['correct']) != 2: + msg = (f'Numeric interval must be a list with two numbers, in ' + f'{self["ref"]}') + raise QuestionException(msg) + + try: + self['correct'] = [float(n) for n in self['correct']] + except Exception: + msg = (f'Numeric interval must be a list with two numbers, in ' + f'{self["ref"]}') + raise QuestionException(msg) + + # invalid + else: + msg = (f'Numeric interval must be a list with two numbers, in ' + f'{self["ref"]}') + raise QuestionException(msg) + # ------------------------------------------------------------------------ def correct(self) -> None: super().correct() @@ -520,23 +615,27 @@ class QFactory(object): if q['type'] == 'generator': logger.debug(f' \\_ Running "{q["script"]}".') q.setdefault('args', []) - q.setdefault('stdin', '') # FIXME is it really necessary? + q.setdefault('stdin', '') script = path.join(q['path'], q['script']) out = await run_script_async(script=script, args=q['args'], stdin=q['stdin']) q.update(out) - # Finally we create an instance of Question() + # Get class for this question type try: - qinstance = self._types[q['type']](QDict(q)) # of matching class - except QuestionException as e: - logger.error(e) - raise e + qclass = self._types[q['type']] except KeyError: logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') raise - else: - return qinstance + + # Finally create an instance of Question() + try: + qinstance = qclass(QDict(q)) + except QuestionException as e: + # logger.error(e) + raise e + + return qinstance # ------------------------------------------------------------------------ def generate(self) -> Question: diff --git a/aprendizations/serve.py b/aprendizations/serve.py index 5ba1bc8..160d9ff 100644 --- a/aprendizations/serve.py +++ b/aprendizations/serve.py @@ -19,6 +19,7 @@ from tornado.escape import to_unicode from .tools import md_to_html from . import APP_NAME + # setup logger for this module logger = logging.getLogger(__name__) @@ -294,10 +295,10 @@ class QuestionHandler(BaseHandler): # --- get question to render @tornado.web.authenticated - def get(self): - logger.debug('[QuestionHandler.get]') + async def get(self): + logger.debug('[QuestionHandler]') user = self.current_user - q = self.learn.get_current_question(user) + q = await self.learn.get_question(user) if q is not None: qhtml = self.render_string(self.templates[q['type']], @@ -326,15 +327,13 @@ class QuestionHandler(BaseHandler): # --- post answer, returns what to do next: shake, new_question, finished @tornado.web.authenticated async def post(self) -> None: - logger.debug('[QuestionHandler.post]') user = self.current_user answer = self.get_body_arguments('answer') # list - logger.debug(f'user = {user}, answer = {answer}') + qid = self.get_body_arguments('qid')[0] + logger.debug(f'[QuestionHandler] answer={answer}') # --- check if browser opened different questions simultaneously - answer_qid = self.get_body_arguments('qid')[0] - current_qid = self.learn.get_current_question_id(user) - if answer_qid != current_qid: + if qid != self.learn.get_current_question_id(user): logger.info(f'User {user} desynchronized questions') self.write({ 'method': 'invalid', @@ -346,7 +345,7 @@ class QuestionHandler(BaseHandler): return # --- brain hacking ;) - await asyncio.sleep(1) + await asyncio.sleep(2) # --- answers are in a list. fix depending on question type qtype = self.learn.get_student_question_type(user) @@ -361,11 +360,11 @@ class QuestionHandler(BaseHandler): ans = answer # --- check answer (nonblocking) and get corrected question and action - q, action = await self.learn.check_answer(user, ans) + q = await self.learn.check_answer(user, ans) # --- built response to return - response = {'method': action, 'params': {}} - if action == 'right': # get next question in the topic + response = {'method': q['status'], 'params': {}} + if q['status'] == 'right': # get next question in the topic comments_html = self.render_string( 'comments-right.html', comments=q['comments'], md=md_to_html) @@ -379,7 +378,7 @@ class QuestionHandler(BaseHandler): 'solution': to_unicode(solution_html), 'tries': q['tries'], } - elif action == 'try_again': + elif q['status'] == 'try_again': comments_html = self.render_string( 'comments.html', comments=q['comments'], md=md_to_html) @@ -389,7 +388,7 @@ class QuestionHandler(BaseHandler): 'comments': to_unicode(comments_html), 'tries': q['tries'], } - elif action == 'wrong': # no more tries + elif q['status'] == 'wrong': # no more tries comments_html = self.render_string( 'comments.html', comments=q['comments'], md=md_to_html) @@ -404,7 +403,7 @@ class QuestionHandler(BaseHandler): 'tries': q['tries'], } else: - logger.error(f'Unknown action: {action}') + logger.error(f'Unknown question status: {q["status"]}') self.write(response) @@ -457,7 +456,7 @@ def run_webserver(app, # --- run webserver signal.signal(signal.SIGINT, signal_handler) - logger.info('Webserver running. (Ctrl-C to stop)') + logger.info('Webserver running... (Ctrl-C to stop)') try: tornado.ioloop.IOLoop.current().start() # running... diff --git a/aprendizations/static/css/topic.css b/aprendizations/static/css/topic.css index 01aae8e..954d256 100644 --- a/aprendizations/static/css/topic.css +++ b/aprendizations/static/css/topic.css @@ -27,3 +27,6 @@ html { border: 1px solid #eee; height: auto; } +label { + display: block; +} diff --git a/aprendizations/student.py b/aprendizations/student.py index 6319bb2..4def72d 100644 --- a/aprendizations/student.py +++ b/aprendizations/student.py @@ -74,11 +74,11 @@ class StudentState(object): logger.debug(f'is locked "{topic}"') return - # starting new topic + # choose k questions self.current_topic = topic + # self.current_question = None self.correct_answers = 0 self.wrong_answers = 0 - t = self.deps.nodes[topic] k = t['choose'] if t['shuffle_questions']: @@ -90,8 +90,7 @@ class StudentState(object): self.questions: List[Question] = [await self.factory[ref].gen_async() for ref in questions] - n = len(self.questions) - logger.debug(f'generated {n} questions') + logger.debug(f'generated {len(self.questions)} questions') # get first question self.next_question() @@ -110,62 +109,70 @@ class StudentState(object): self.wrong_answers) } self.current_topic = None + self.current_question = None self.unlock_topics() # ------------------------------------------------------------------------ - # corrects current question with provided answer. - # implements the logic: - # - if answer ok, goes to next question - # - if wrong, counts number of tries. If exceeded, moves on. + # corrects current question # ------------------------------------------------------------------------ async def check_answer(self, answer) -> Tuple[Question, str]: - q: Question = self.current_question + q = self.current_question q['answer'] = answer q['finish_time'] = datetime.now() - logger.debug(f'checking answer of {q["ref"]}...') await q.correct_async() - logger.debug(f'grade = {q["grade"]:.2}') if q['grade'] > 0.999: self.correct_answers += 1 - self.next_question() - action = 'right' + q['status'] = 'right' else: self.wrong_answers += 1 - self.current_question['tries'] -= 1 - - if self.current_question['tries'] > 0: - action = 'try_again' + q['tries'] -= 1 + if q['tries'] > 0: + q['status'] = 'try_again' else: - action = 'wrong' - if self.current_question['append_wrong']: - logger.debug('wrong answer, append new question') - # self.questions.append(self.factory[q['ref']].generate()) - new_question = await self.factory[q['ref']].gen_async() - self.questions.append(new_question) - self.next_question() + q['status'] = 'wrong' - # returns corrected question (not new one) which might include comments - return q, action + logger.debug(f'ref = {q["ref"]}, status = {q["status"]}') + return q # ------------------------------------------------------------------------ - # Move to next question, or None + # get question to show, current or next # ------------------------------------------------------------------------ - def next_question(self) -> Optional[Question]: + async def get_question(self) -> Optional[Question]: + q = self.current_question + logger.debug(f'{q["ref"]} status = {q["status"]}') + + if q['status'] == 'right': + self.next_question() + elif q['status'] == 'wrong': + if q['append_wrong']: + logger.debug(' wrong answer => append new question') + new_question = await self.factory[q['ref']].gen_async() + self.questions.append(new_question) + self.next_question() + # elif q['status'] == 'new': + # pass + # elif q['status'] == 'try_again': + # pass + + return self.current_question + + # ------------------------------------------------------------------------ + # moves to next question + # ------------------------------------------------------------------------ + def next_question(self) -> None: try: - self.current_question = self.questions.pop(0) + q = self.questions.pop(0) except IndexError: - self.current_question = None self.finish_topic() - else: - self.current_question['start_time'] = datetime.now() - default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] - maxtries = self.current_question.get('max_tries', default_maxtries) - self.current_question['tries'] = maxtries - logger.debug(f'current_question = {self.current_question["ref"]}') + return - return self.current_question # question or None + t = self.deps.nodes[self.current_topic] + q['start_time'] = datetime.now() + q['tries'] = q.get('max_tries', t['max_tries']) + q['status'] = 'new' + self.current_question = q # ------------------------------------------------------------------------ # Update proficiency level of the topics using a forgetting factor @@ -178,7 +185,7 @@ class StudentState(object): forgetting_factor = self.deps.nodes[tref]['forgetting_factor'] s['level'] *= forgetting_factor ** dt.days # forgetting factor except KeyError: - logger.warning(f'Topic {tref} is not on the graph!') + logger.warning(f'Update topic levels: {tref} not in the graph') # ------------------------------------------------------------------------ # Unlock topics whose dependencies are satisfied (> min_level) diff --git a/aprendizations/templates/question-checkbox.html b/aprendizations/templates/question-checkbox.html index 78e3fd2..b16f7a7 100644 --- a/aprendizations/templates/question-checkbox.html +++ b/aprendizations/templates/question-checkbox.html @@ -9,7 +9,9 @@
- +
{% end %} diff --git a/aprendizations/templates/question-radio.html b/aprendizations/templates/question-radio.html index 6d13057..763c5b8 100644 --- a/aprendizations/templates/question-radio.html +++ b/aprendizations/templates/question-radio.html @@ -9,7 +9,9 @@
- +
{% end %} diff --git a/config/logger-debug.yaml b/config/logger-debug.yaml index 45d6b09..96053d5 100644 --- a/config/logger-debug.yaml +++ b/config/logger-debug.yaml @@ -2,50 +2,52 @@ version: 1 formatters: - void: - format: '' - standard: - format: '%(asctime)s | %(thread)-15d | %(levelname)-8s | %(module)-10s | %(funcName)-20s | %(message)s' - # datefmt: '%H:%M:%S' + void: + format: '' + standard: + format: '%(asctime)s | %(levelname)-8s | %(module)-10s | %(funcName)-22s | + %(message)s' + # | %(thread)-15d + # datefmt: '%H:%M:%S' handlers: - default: - level: 'DEBUG' - class: 'logging.StreamHandler' - formatter: 'standard' - stream: 'ext://sys.stdout' + default: + level: 'DEBUG' + class: 'logging.StreamHandler' + formatter: 'standard' + stream: 'ext://sys.stdout' loggers: - '': - handlers: ['default'] - level: 'DEBUG' - - 'aprendizations.factory': - handlers: ['default'] - level: 'DEBUG' - propagate: false - - 'aprendizations.student': - handlers: ['default'] - level: 'DEBUG' - propagate: false - - 'aprendizations.learnapp': - handlers: ['default'] - level: 'DEBUG' - propagate: false - - 'aprendizations.questions': - handlers: ['default'] - level: 'DEBUG' - propagate: false - - 'aprendizations.tools': - handlers: ['default'] - level: 'DEBUG' - propagate: false - - 'aprendizations.serve': - handlers: ['default'] - level: 'DEBUG' - propagate: false + '': + handlers: ['default'] + level: 'DEBUG' + + 'aprendizations.factory': + handlers: ['default'] + level: 'DEBUG' + propagate: false + + 'aprendizations.student': + handlers: ['default'] + level: 'DEBUG' + propagate: false + + 'aprendizations.learnapp': + handlers: ['default'] + level: 'DEBUG' + propagate: false + + 'aprendizations.questions': + handlers: ['default'] + level: 'DEBUG' + propagate: false + + 'aprendizations.tools': + handlers: ['default'] + level: 'DEBUG' + propagate: false + + 'aprendizations.serve': + handlers: ['default'] + level: 'DEBUG' + propagate: false diff --git a/demo/astronomy.yaml b/demo/astronomy.yaml index 2f2a4a2..e1bf297 100644 --- a/demo/astronomy.yaml +++ b/demo/astronomy.yaml @@ -3,11 +3,11 @@ # optional values applied to each topic, if undefined there # ---------------------------------------------------------------------------- -# defaults: +# defaults: FIXME not working # file: questions.yaml # shuffle_questions: true # choose: 6 -# max_tries: 2 +# max_tries: 200 # forgetting_factor: 0.97 # min_level: 0.01 # append_wrong: true diff --git a/demo/astronomy/solar-system/questions.yaml b/demo/astronomy/solar-system/questions.yaml index f77979e..c3fdbb7 100644 --- a/demo/astronomy/solar-system/questions.yaml +++ b/demo/astronomy/solar-system/questions.yaml @@ -28,6 +28,7 @@ - Têm todos o mesmo tamanho # opcional correct: 2 + # discount: true shuffle: false solution: | O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas diff --git a/demo/math/addition/questions.yaml b/demo/math/addition/questions.yaml index 0ab7d95..903dace 100644 --- a/demo/math/addition/questions.yaml +++ b/demo/math/addition/questions.yaml @@ -5,6 +5,7 @@ script: addition-two-digits.py args: [10, 20] +# --------------------------------------------------------------------------- - type: checkbox ref: addition-properties title: Propriedades da adição @@ -17,6 +18,6 @@ - Propriedade comutativa, $x+y=y+x$. # wrong - Existência de elemento absorvente, $x+1=1$. - correct: [1, 1, 1, 1, -1] + correct: [1, 1, 1, 1, 0] solution: | A adição não tem elemento absorvente. diff --git a/package-lock.json b/package-lock.json index c2a9df5..406ffc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,244 +2,26 @@ "requires": true, "lockfileVersion": 1, "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/core": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.6.0.tgz", - "integrity": "sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.0", - "@babel/helpers": "^7.6.0", - "@babel/parser": "^7.6.0", - "@babel/template": "^7.6.0", - "@babel/traverse": "^7.6.0", - "@babel/types": "^7.6.0", - "convert-source-map": "^1.1.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - } - }, - "@babel/generator": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz", - "integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==", - "requires": { - "@babel/types": "^7.6.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/helpers": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.6.0.tgz", - "integrity": "sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ==", - "requires": { - "@babel/template": "^7.6.0", - "@babel/traverse": "^7.6.0", - "@babel/types": "^7.6.0" - } - }, - "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", - "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" - }, - "@babel/template": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", - "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.0" - } - }, - "@babel/traverse": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz", - "integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.0", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, "@fortawesome/fontawesome-free": { "version": "5.11.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz", "integrity": "sha512-XiUPoS79r1G7PcpnNtq85TJ7inJWe0v+b5oZJZKb0pGHNIV6+UiNeQWiFGmuQ0aj7GEhnD/v9iqxIsjuRKtEnQ==" }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "codemirror": { - "version": "5.49.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.0.tgz", - "integrity": "sha512-Hyzr0HToBdZpLBN9dYFO/KlJAsKH37/cXVHPAqa+imml0R92tb9AkmsvjnXL+SluEvjjdfkDgRjc65NG5jnMYA==" - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.2.tgz", + "integrity": "sha512-dwJ2HRPHm8w51WB5YTF9J7m6Z5dtkqbU9ntMZ1dqXyFB9IpjoUFDj80ahRVEoVanfIp6pfASJbOlbWdEf8FOzQ==" }, "commander": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.1.tgz", "integrity": "sha512-UNgvDd+csKdc9GD4zjtkHKQbT8Aspt2jCBqNSPp53vAS0L1tS9sXB2TCEOPHJ7kt9bN/niWkYj8T3RQSoMXdSQ==" }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, "esm": { "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "requires": { - "minimist": "^1.2.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - }, "mathjax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.0.0.tgz", @@ -258,56 +40,15 @@ } }, "mdbootstrap": { - "version": "4.8.10", - "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.8.10.tgz", - "integrity": "sha512-pUjs7Vds4J+MwepOo4obUy7bQ5aMeB8j1c3IxIcEYXOXmn8GOWMSpiRcfSXpH9R4Fgdfie++e0fm5+SebRnTYA==", - "requires": { - "@babel/core": "^7.3.3" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.9.0.tgz", + "integrity": "sha512-6R3j5D9Qmp+Aa90FblOVAwVDSqpAICYW2dpNxh6uaVB9E9MCaBLdaTKLrXCB7xznReHEaA57pNABXgFoi2z7Rg==" }, "mj-context-menu": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.2.0.tgz", "integrity": "sha512-yJxrWBHCjFZEHsZgfs7m5g9OSCNzsVYadW6f6lX3pgZL67vmodtSW/4zhsYmuDKweXfHs0M1kJge1uQIasWA+g==" }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "requires": { - "path-parse": "^1.0.6" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, "speech-rule-engine": { "version": "3.0.0-beta.6", "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.0.0-beta.6.tgz", @@ -318,24 +59,6 @@ "xmldom-sre": "^0.1.31" } }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" - }, "wicked-good-xpath": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", diff --git a/package.json b/package.json index f2445b8..1414bbe 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "email": "mjsb@uevora.pt", "dependencies": { "@fortawesome/fontawesome-free": "^5.11.2", - "codemirror": "^5.49.0", + "codemirror": "^5.49.2", "mathjax": "^3", - "mdbootstrap": "^4.8.10" + "mdbootstrap": "^4.9.0" }, "private": true } -- libgit2 0.21.2