diff --git a/aprendizations/initdb.py b/aprendizations/initdb.py index b906a77..14c4fb9 100644 --- a/aprendizations/initdb.py +++ b/aprendizations/initdb.py @@ -134,7 +134,7 @@ def main(): print(f'Database: {args.db}') engine = create_engine(f'sqlite:///{args.db}', echo=False, future=True) Base.metadata.create_all(engine) # Creates schema if needed - session = Session(engine) + session = Session(engine, future=True) # --- build list of students to insert/update students = [] diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index 2d0fbf3..f43765f 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -16,8 +16,9 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict # third party libraries import bcrypt import networkx as nx -from sqlalchemy import create_engine, select +from sqlalchemy import create_engine, select, func from sqlalchemy.orm import Session +from sqlalchemy.exc import NoResultFound # this project from aprendizations.models import Student, Answer, Topic, StudentTopic @@ -55,25 +56,6 @@ class LearnApp(): 'counter': ...}, ...} ''' - - # ------------------------------------------------------------------------ - # @contextmanager - # def _db_session(self, **kw): - # ''' - # helper to manage db sessions using the `with` statement, for example - # with self._db_session() as s: s.query(...) - # ''' - # session = self.Session(**kw) - # try: - # yield session - # session.commit() - # except Exception: - # logger.error('!!! Database rollback !!!') - # session.rollback() - # raise - # finally: - # session.close() - # ------------------------------------------------------------------------ def __init__(self, courses: str, # filename with course configurations @@ -135,7 +117,7 @@ class LearnApp(): # ------------------------------------------------------------------------ def _sanity_check_questions(self) -> None: ''' - Unity tests for all questions + Unit tests for all questions Generates all questions, give right and wrong answers and corrects. ''' @@ -173,7 +155,7 @@ class LearnApp(): continue # to next test if errors > 0: - logger.error('%6d error(s) found.', errors) # {errors:>6} + logger.error('%6d error(s) found.', errors) raise LearnException('Sanity checks') logger.info(' 0 errors found.') @@ -181,26 +163,22 @@ class LearnApp(): async def login(self, uid: str, password: str) -> bool: '''user login''' - with self._db_session() as sess: - found = sess.query(Student.name, Student.password) \ - .filter_by(id=uid) \ - .one_or_none() - # wait random time to minimize timing attacks await asyncio.sleep(random()) - loop = asyncio.get_running_loop() - if found is None: + query = select(Student).where(Student.id == uid) + try: + with Session(self._engine, future=True) as session: + student = session.execute(query).scalar_one() + except NoResultFound: logger.info('User "%s" does not exist', uid) - await loop.run_in_executor(None, bcrypt.hashpw, b'', - bcrypt.gensalt()) # just spend time return False - name, hashed_pw = found + loop = asyncio.get_running_loop() pw_ok: bool = await loop.run_in_executor(None, bcrypt.checkpw, password.encode('utf-8'), - hashed_pw) + student.password) if pw_ok: if uid in self.online: @@ -210,10 +188,10 @@ class LearnApp(): logger.info('User "%s" logged in', uid) counter = 0 - # get topics of this student and set its current state - with self._db_session() as sess: - student_topics = sess.query(StudentTopic) \ - .filter_by(student_id=uid) + # get topics for this student and set its current state + query = select(StudentTopic).where(StudentTopic.student_id == uid) + with Session(self._engine, future=True) as session: + student_topics = session.execute(query).scalars().all() state = {t.topic_id: { 'level': t.level, @@ -222,7 +200,7 @@ class LearnApp(): self.online[uid] = { 'number': uid, - 'name': name, + 'name': student.name, 'state': StudentState(uid=uid, state=state, courses=self.courses, deps=self.deps, factory=self.factory), @@ -250,14 +228,16 @@ class LearnApp(): return False loop = asyncio.get_running_loop() - pw = await loop.run_in_executor(None, - bcrypt.hashpw, - password.encode('utf-8'), - bcrypt.gensalt()) + hashed_pw = await loop.run_in_executor(None, + bcrypt.hashpw, + password.encode('utf-8'), + bcrypt.gensalt()) - with self._db_session() as sess: - user = sess.query(Student).get(uid) - user.password = pw + with Session(self._engine, future=True) as session: + query = select(Student).where(Student.id == uid) + user = session.execute(query).scalar_one() + user.password = hashed_pw + session.commit() logger.info('User "%s" changed password', uid) return True @@ -279,13 +259,15 @@ class LearnApp(): logger.info('User "%s" got %.2f in "%s"', uid, grade, ref) # always save grade of answered question - with self._db_session() as sess: - sess.add(Answer(ref=ref, - grade=grade, - starttime=str(question['start_time']), - finishtime=str(question['finish_time']), - student_id=uid, - topic_id=topic_id)) + answer = Answer(ref=ref, + grade=grade, + starttime=str(question['start_time']), + finishtime=str(question['finish_time']), + student_id=uid, + topic_id=topic_id) + with Session(self._engine, future=True) as session: + session.add(answer) + session.commit() return question @@ -295,38 +277,45 @@ class LearnApp(): Get the question to show (current or new one) If no more questions, save/update level in database ''' - student = self.online[uid]['state'] - question: Optional[Question] = await student.get_question() + student_state = self.online[uid]['state'] + question: Optional[Question] = await student_state.get_question() # save topic to database if finished - if student.topic_has_finished(): - topic: str = student.get_previous_topic() - level: float = student.get_topic_level(topic) - date: str = str(student.get_topic_date(topic)) + if student_state.topic_has_finished(): + topic_id: str = student_state.get_previous_topic() + level: float = student_state.get_topic_level(topic_id) + date: str = str(student_state.get_topic_date(topic_id)) logger.info('User "%s" finished "%s" (level=%.2f)', - uid, topic, level) + uid, topic_id, level) - with self._db_session() as sess: - student_topic = sess.query(StudentTopic) \ - .filter_by(student_id=uid, topic_id=topic)\ - .one_or_none() + query = select(StudentTopic).where( + StudentTopic.student_id == uid and + StudentTopic.topic_id == topic_id + ) + with Session(self._engine, future=True) as session: + student_topic = session.execute(query).scalar_one_or_none() if student_topic is None: # insert new studenttopic into database logger.debug('db insert studenttopic') - tid = sess.query(Topic).get(topic) - uid = sess.query(Student).get(uid) + query_topic = select(Topic).where(Topic.id == topic_id) + query_student = select(Student).where(Student.id == uid) + topic = session.execute(query_topic).scalar_one() + student = session.execute(query_student).scalar_one() # association object - student_topic = StudentTopic(level=level, date=date, - topic=tid, student=uid) - uid.topics.append(student_topic) + student_topic = StudentTopic(level=level, + date=date, + topic=topic, + student=student) + student.topics.append(student_topic) else: # update studenttopic in database logger.debug('db update studenttopic to level %f', level) student_topic.level = level student_topic.date = date - sess.add(student_topic) + session.add(student_topic) + session.commit() return question @@ -334,9 +323,9 @@ class LearnApp(): def start_course(self, uid: str, course_id: str) -> None: '''Start course''' - student = self.online[uid]['state'] + student_state = self.online[uid]['state'] try: - student.start_course(course_id) + student_state.start_course(course_id) except Exception as exc: logger.warning('"%s" could not start course "%s"', uid, course_id) raise LearnException() from exc @@ -369,7 +358,7 @@ class LearnApp(): ''' Fill db table 'Topic' with topics from the graph, if new ''' - with Session(self._engine) as session: + with Session(self._engine, future=True) as session: db_topics = session.execute(select(Topic.id)).scalars().all() new = [Topic(id=t) for t in topics if t not in db_topics] if new: @@ -389,11 +378,16 @@ class LearnApp(): 'Use "initdb-aprendizations" to create') self._engine = create_engine(f'sqlite:///{database}', future=True) + + try: - with Session(self._engine) as session: - count_students: int = session.query(Student).count() - count_topics: int = session.query(Topic).count() - count_answers: int = session.query(Answer).count() + query_students = select(func.count(Student.id)) + query_topics = select(func.count(Topic.id)) + query_answers = select(func.count(Answer.id)) + with Session(self._engine, future=True) as session: + count_students = session.execute(query_students).scalar() + count_topics = session.execute(query_topics).scalar() + count_answers = session.execute(query_answers).scalar() except Exception as exc: logger.error('Database "%s" not usable!', database) raise DatabaseUnusableError() from exc @@ -615,29 +609,27 @@ class LearnApp(): the rankings. This is so that there can be users for development or testing purposes, which are not real users. The user_id of real students must have >2 chars. + This should be modified to have a "visible" flag ''' logger.info('User "%s" get rankings for %s', uid, course_id) - with self._db_session() as sess: + query_students = select(Student.id, Student.name) + query_student_topics = select(StudentTopic.student_id, + StudentTopic.topic_id, + StudentTopic.level, + StudentTopic.date) + query_total = select(Answer.student_id, func.count(Answer.ref)) + query_right = select(Answer.student_id, func.count(Answer.ref)).where(Answer.grade == 1.0) + with Session(self._engine, future=True) as session: # all students in the database FIXME only with answers of this course - students = sess.query(Student.id, Student.name).all() + students = session.execute(query_students).all() # topic levels FIXME only topics of this course - student_topics = sess.query(StudentTopic.student_id, - StudentTopic.topic_id, - StudentTopic.level, - StudentTopic.date).all() + student_topics = session.execute(query_student_topics).all() # answer performance - total = dict(sess.query(Answer.student_id, - sa.func.count(Answer.ref)) \ - .group_by(Answer.student_id) \ - .all()) - right = dict(sess.query(Answer.student_id, - sa.func.count(Answer.ref)) \ - .filter(Answer.grade == 1.0) \ - .group_by(Answer.student_id) \ - .all()) + total = dict(session.execute(query_total).all()) + right = dict(session.execute(query_right).all()) # compute percentage of right answers perf: Dict[str, float] = {u: right.get(u, 0.0) / total[u] diff --git a/demo/math/multiplication/multiplication-table.py b/demo/math/multiplication/multiplication-table.py index b3a9672..e898518 100755 --- a/demo/math/multiplication/multiplication-table.py +++ b/demo/math/multiplication/multiplication-table.py @@ -1,13 +1,7 @@ #!/usr/bin/env python3 import random -import sys -# can be repeated -# x = random.randint(2, 9) -# y = random.randint(2, 9) - -# with x != y x, y = random.sample(range(2,10), k=2) r = x * y @@ -19,6 +13,7 @@ type: text title: Multiplicação (tabuada) text: | Qual o resultado da multiplicação ${x}\\times {y}$? +transform: ['trim'] correct: ['{r}'] solution: | A multiplicação é a repetição da soma. Podemos fazer de duas maneiras: diff --git a/demo/math/multiplication/questions.yaml b/demo/math/multiplication/questions.yaml index cb8ac9e..3efa45c 100644 --- a/demo/math/multiplication/questions.yaml +++ b/demo/math/multiplication/questions.yaml @@ -1,9 +1,10 @@ --- -# --------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- - type: generator ref: multiplication-table script: multiplication-table.py +# ---------------------------------------------------------------------------- - type: checkbox ref: multiplication-properties title: Propriedades da multiplicação @@ -17,7 +18,7 @@ # wrong - Existência de inverso, todos os números $x$ tem um inverso $1/x$ tal que $x(1/x)=1$. - correct: [1, 1, 1, 1, -1] + correct: [1, 1, 1, 1, 0] solution: | Na multiplicação nem todos os números têm inverso. Só têm inverso os números diferentes de zero. -- libgit2 0.21.2