Commit 547f01489590e33dc6131514352b7653d04309c5
1 parent
97085de7
Exists in
master
and in
1 other branch
finished convertion to sqlalchemy 1.4/2.0
Showing
4 changed files
with
88 additions
and
100 deletions
Show diff stats
aprendizations/initdb.py
| @@ -134,7 +134,7 @@ def main(): | @@ -134,7 +134,7 @@ def main(): | ||
| 134 | print(f'Database: {args.db}') | 134 | print(f'Database: {args.db}') |
| 135 | engine = create_engine(f'sqlite:///{args.db}', echo=False, future=True) | 135 | engine = create_engine(f'sqlite:///{args.db}', echo=False, future=True) |
| 136 | Base.metadata.create_all(engine) # Creates schema if needed | 136 | Base.metadata.create_all(engine) # Creates schema if needed |
| 137 | - session = Session(engine) | 137 | + session = Session(engine, future=True) |
| 138 | 138 | ||
| 139 | # --- build list of students to insert/update | 139 | # --- build list of students to insert/update |
| 140 | students = [] | 140 | students = [] |
aprendizations/learnapp.py
| @@ -16,8 +16,9 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict | @@ -16,8 +16,9 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict | ||
| 16 | # third party libraries | 16 | # third party libraries |
| 17 | import bcrypt | 17 | import bcrypt |
| 18 | import networkx as nx | 18 | import networkx as nx |
| 19 | -from sqlalchemy import create_engine, select | 19 | +from sqlalchemy import create_engine, select, func |
| 20 | from sqlalchemy.orm import Session | 20 | from sqlalchemy.orm import Session |
| 21 | +from sqlalchemy.exc import NoResultFound | ||
| 21 | 22 | ||
| 22 | # this project | 23 | # this project |
| 23 | from aprendizations.models import Student, Answer, Topic, StudentTopic | 24 | from aprendizations.models import Student, Answer, Topic, StudentTopic |
| @@ -55,25 +56,6 @@ class LearnApp(): | @@ -55,25 +56,6 @@ class LearnApp(): | ||
| 55 | 'counter': ...}, ...} | 56 | 'counter': ...}, ...} |
| 56 | ''' | 57 | ''' |
| 57 | 58 | ||
| 58 | - | ||
| 59 | - # ------------------------------------------------------------------------ | ||
| 60 | - # @contextmanager | ||
| 61 | - # def _db_session(self, **kw): | ||
| 62 | - # ''' | ||
| 63 | - # helper to manage db sessions using the `with` statement, for example | ||
| 64 | - # with self._db_session() as s: s.query(...) | ||
| 65 | - # ''' | ||
| 66 | - # session = self.Session(**kw) | ||
| 67 | - # try: | ||
| 68 | - # yield session | ||
| 69 | - # session.commit() | ||
| 70 | - # except Exception: | ||
| 71 | - # logger.error('!!! Database rollback !!!') | ||
| 72 | - # session.rollback() | ||
| 73 | - # raise | ||
| 74 | - # finally: | ||
| 75 | - # session.close() | ||
| 76 | - | ||
| 77 | # ------------------------------------------------------------------------ | 59 | # ------------------------------------------------------------------------ |
| 78 | def __init__(self, | 60 | def __init__(self, |
| 79 | courses: str, # filename with course configurations | 61 | courses: str, # filename with course configurations |
| @@ -135,7 +117,7 @@ class LearnApp(): | @@ -135,7 +117,7 @@ class LearnApp(): | ||
| 135 | # ------------------------------------------------------------------------ | 117 | # ------------------------------------------------------------------------ |
| 136 | def _sanity_check_questions(self) -> None: | 118 | def _sanity_check_questions(self) -> None: |
| 137 | ''' | 119 | ''' |
| 138 | - Unity tests for all questions | 120 | + Unit tests for all questions |
| 139 | 121 | ||
| 140 | Generates all questions, give right and wrong answers and corrects. | 122 | Generates all questions, give right and wrong answers and corrects. |
| 141 | ''' | 123 | ''' |
| @@ -173,7 +155,7 @@ class LearnApp(): | @@ -173,7 +155,7 @@ class LearnApp(): | ||
| 173 | continue # to next test | 155 | continue # to next test |
| 174 | 156 | ||
| 175 | if errors > 0: | 157 | if errors > 0: |
| 176 | - logger.error('%6d error(s) found.', errors) # {errors:>6} | 158 | + logger.error('%6d error(s) found.', errors) |
| 177 | raise LearnException('Sanity checks') | 159 | raise LearnException('Sanity checks') |
| 178 | logger.info(' 0 errors found.') | 160 | logger.info(' 0 errors found.') |
| 179 | 161 | ||
| @@ -181,26 +163,22 @@ class LearnApp(): | @@ -181,26 +163,22 @@ class LearnApp(): | ||
| 181 | async def login(self, uid: str, password: str) -> bool: | 163 | async def login(self, uid: str, password: str) -> bool: |
| 182 | '''user login''' | 164 | '''user login''' |
| 183 | 165 | ||
| 184 | - with self._db_session() as sess: | ||
| 185 | - found = sess.query(Student.name, Student.password) \ | ||
| 186 | - .filter_by(id=uid) \ | ||
| 187 | - .one_or_none() | ||
| 188 | - | ||
| 189 | # wait random time to minimize timing attacks | 166 | # wait random time to minimize timing attacks |
| 190 | await asyncio.sleep(random()) | 167 | await asyncio.sleep(random()) |
| 191 | 168 | ||
| 192 | - loop = asyncio.get_running_loop() | ||
| 193 | - if found is None: | 169 | + query = select(Student).where(Student.id == uid) |
| 170 | + try: | ||
| 171 | + with Session(self._engine, future=True) as session: | ||
| 172 | + student = session.execute(query).scalar_one() | ||
| 173 | + except NoResultFound: | ||
| 194 | logger.info('User "%s" does not exist', uid) | 174 | logger.info('User "%s" does not exist', uid) |
| 195 | - await loop.run_in_executor(None, bcrypt.hashpw, b'', | ||
| 196 | - bcrypt.gensalt()) # just spend time | ||
| 197 | return False | 175 | return False |
| 198 | 176 | ||
| 199 | - name, hashed_pw = found | 177 | + loop = asyncio.get_running_loop() |
| 200 | pw_ok: bool = await loop.run_in_executor(None, | 178 | pw_ok: bool = await loop.run_in_executor(None, |
| 201 | bcrypt.checkpw, | 179 | bcrypt.checkpw, |
| 202 | password.encode('utf-8'), | 180 | password.encode('utf-8'), |
| 203 | - hashed_pw) | 181 | + student.password) |
| 204 | 182 | ||
| 205 | if pw_ok: | 183 | if pw_ok: |
| 206 | if uid in self.online: | 184 | if uid in self.online: |
| @@ -210,10 +188,10 @@ class LearnApp(): | @@ -210,10 +188,10 @@ class LearnApp(): | ||
| 210 | logger.info('User "%s" logged in', uid) | 188 | logger.info('User "%s" logged in', uid) |
| 211 | counter = 0 | 189 | counter = 0 |
| 212 | 190 | ||
| 213 | - # get topics of this student and set its current state | ||
| 214 | - with self._db_session() as sess: | ||
| 215 | - student_topics = sess.query(StudentTopic) \ | ||
| 216 | - .filter_by(student_id=uid) | 191 | + # get topics for this student and set its current state |
| 192 | + query = select(StudentTopic).where(StudentTopic.student_id == uid) | ||
| 193 | + with Session(self._engine, future=True) as session: | ||
| 194 | + student_topics = session.execute(query).scalars().all() | ||
| 217 | 195 | ||
| 218 | state = {t.topic_id: { | 196 | state = {t.topic_id: { |
| 219 | 'level': t.level, | 197 | 'level': t.level, |
| @@ -222,7 +200,7 @@ class LearnApp(): | @@ -222,7 +200,7 @@ class LearnApp(): | ||
| 222 | 200 | ||
| 223 | self.online[uid] = { | 201 | self.online[uid] = { |
| 224 | 'number': uid, | 202 | 'number': uid, |
| 225 | - 'name': name, | 203 | + 'name': student.name, |
| 226 | 'state': StudentState(uid=uid, state=state, | 204 | 'state': StudentState(uid=uid, state=state, |
| 227 | courses=self.courses, deps=self.deps, | 205 | courses=self.courses, deps=self.deps, |
| 228 | factory=self.factory), | 206 | factory=self.factory), |
| @@ -250,14 +228,16 @@ class LearnApp(): | @@ -250,14 +228,16 @@ class LearnApp(): | ||
| 250 | return False | 228 | return False |
| 251 | 229 | ||
| 252 | loop = asyncio.get_running_loop() | 230 | loop = asyncio.get_running_loop() |
| 253 | - pw = await loop.run_in_executor(None, | ||
| 254 | - bcrypt.hashpw, | ||
| 255 | - password.encode('utf-8'), | ||
| 256 | - bcrypt.gensalt()) | 231 | + hashed_pw = await loop.run_in_executor(None, |
| 232 | + bcrypt.hashpw, | ||
| 233 | + password.encode('utf-8'), | ||
| 234 | + bcrypt.gensalt()) | ||
| 257 | 235 | ||
| 258 | - with self._db_session() as sess: | ||
| 259 | - user = sess.query(Student).get(uid) | ||
| 260 | - user.password = pw | 236 | + with Session(self._engine, future=True) as session: |
| 237 | + query = select(Student).where(Student.id == uid) | ||
| 238 | + user = session.execute(query).scalar_one() | ||
| 239 | + user.password = hashed_pw | ||
| 240 | + session.commit() | ||
| 261 | 241 | ||
| 262 | logger.info('User "%s" changed password', uid) | 242 | logger.info('User "%s" changed password', uid) |
| 263 | return True | 243 | return True |
| @@ -279,13 +259,15 @@ class LearnApp(): | @@ -279,13 +259,15 @@ class LearnApp(): | ||
| 279 | logger.info('User "%s" got %.2f in "%s"', uid, grade, ref) | 259 | logger.info('User "%s" got %.2f in "%s"', uid, grade, ref) |
| 280 | 260 | ||
| 281 | # always save grade of answered question | 261 | # always save grade of answered question |
| 282 | - with self._db_session() as sess: | ||
| 283 | - sess.add(Answer(ref=ref, | ||
| 284 | - grade=grade, | ||
| 285 | - starttime=str(question['start_time']), | ||
| 286 | - finishtime=str(question['finish_time']), | ||
| 287 | - student_id=uid, | ||
| 288 | - topic_id=topic_id)) | 262 | + answer = Answer(ref=ref, |
| 263 | + grade=grade, | ||
| 264 | + starttime=str(question['start_time']), | ||
| 265 | + finishtime=str(question['finish_time']), | ||
| 266 | + student_id=uid, | ||
| 267 | + topic_id=topic_id) | ||
| 268 | + with Session(self._engine, future=True) as session: | ||
| 269 | + session.add(answer) | ||
| 270 | + session.commit() | ||
| 289 | 271 | ||
| 290 | return question | 272 | return question |
| 291 | 273 | ||
| @@ -295,38 +277,45 @@ class LearnApp(): | @@ -295,38 +277,45 @@ class LearnApp(): | ||
| 295 | Get the question to show (current or new one) | 277 | Get the question to show (current or new one) |
| 296 | If no more questions, save/update level in database | 278 | If no more questions, save/update level in database |
| 297 | ''' | 279 | ''' |
| 298 | - student = self.online[uid]['state'] | ||
| 299 | - question: Optional[Question] = await student.get_question() | 280 | + student_state = self.online[uid]['state'] |
| 281 | + question: Optional[Question] = await student_state.get_question() | ||
| 300 | 282 | ||
| 301 | # save topic to database if finished | 283 | # save topic to database if finished |
| 302 | - if student.topic_has_finished(): | ||
| 303 | - topic: str = student.get_previous_topic() | ||
| 304 | - level: float = student.get_topic_level(topic) | ||
| 305 | - date: str = str(student.get_topic_date(topic)) | 284 | + if student_state.topic_has_finished(): |
| 285 | + topic_id: str = student_state.get_previous_topic() | ||
| 286 | + level: float = student_state.get_topic_level(topic_id) | ||
| 287 | + date: str = str(student_state.get_topic_date(topic_id)) | ||
| 306 | logger.info('User "%s" finished "%s" (level=%.2f)', | 288 | logger.info('User "%s" finished "%s" (level=%.2f)', |
| 307 | - uid, topic, level) | 289 | + uid, topic_id, level) |
| 308 | 290 | ||
| 309 | - with self._db_session() as sess: | ||
| 310 | - student_topic = sess.query(StudentTopic) \ | ||
| 311 | - .filter_by(student_id=uid, topic_id=topic)\ | ||
| 312 | - .one_or_none() | 291 | + query = select(StudentTopic).where( |
| 292 | + StudentTopic.student_id == uid and | ||
| 293 | + StudentTopic.topic_id == topic_id | ||
| 294 | + ) | ||
| 295 | + with Session(self._engine, future=True) as session: | ||
| 296 | + student_topic = session.execute(query).scalar_one_or_none() | ||
| 313 | 297 | ||
| 314 | if student_topic is None: | 298 | if student_topic is None: |
| 315 | # insert new studenttopic into database | 299 | # insert new studenttopic into database |
| 316 | logger.debug('db insert studenttopic') | 300 | logger.debug('db insert studenttopic') |
| 317 | - tid = sess.query(Topic).get(topic) | ||
| 318 | - uid = sess.query(Student).get(uid) | 301 | + query_topic = select(Topic).where(Topic.id == topic_id) |
| 302 | + query_student = select(Student).where(Student.id == uid) | ||
| 303 | + topic = session.execute(query_topic).scalar_one() | ||
| 304 | + student = session.execute(query_student).scalar_one() | ||
| 319 | # association object | 305 | # association object |
| 320 | - student_topic = StudentTopic(level=level, date=date, | ||
| 321 | - topic=tid, student=uid) | ||
| 322 | - uid.topics.append(student_topic) | 306 | + student_topic = StudentTopic(level=level, |
| 307 | + date=date, | ||
| 308 | + topic=topic, | ||
| 309 | + student=student) | ||
| 310 | + student.topics.append(student_topic) | ||
| 323 | else: | 311 | else: |
| 324 | # update studenttopic in database | 312 | # update studenttopic in database |
| 325 | logger.debug('db update studenttopic to level %f', level) | 313 | logger.debug('db update studenttopic to level %f', level) |
| 326 | student_topic.level = level | 314 | student_topic.level = level |
| 327 | student_topic.date = date | 315 | student_topic.date = date |
| 328 | 316 | ||
| 329 | - sess.add(student_topic) | 317 | + session.add(student_topic) |
| 318 | + session.commit() | ||
| 330 | 319 | ||
| 331 | return question | 320 | return question |
| 332 | 321 | ||
| @@ -334,9 +323,9 @@ class LearnApp(): | @@ -334,9 +323,9 @@ class LearnApp(): | ||
| 334 | def start_course(self, uid: str, course_id: str) -> None: | 323 | def start_course(self, uid: str, course_id: str) -> None: |
| 335 | '''Start course''' | 324 | '''Start course''' |
| 336 | 325 | ||
| 337 | - student = self.online[uid]['state'] | 326 | + student_state = self.online[uid]['state'] |
| 338 | try: | 327 | try: |
| 339 | - student.start_course(course_id) | 328 | + student_state.start_course(course_id) |
| 340 | except Exception as exc: | 329 | except Exception as exc: |
| 341 | logger.warning('"%s" could not start course "%s"', uid, course_id) | 330 | logger.warning('"%s" could not start course "%s"', uid, course_id) |
| 342 | raise LearnException() from exc | 331 | raise LearnException() from exc |
| @@ -369,7 +358,7 @@ class LearnApp(): | @@ -369,7 +358,7 @@ class LearnApp(): | ||
| 369 | ''' | 358 | ''' |
| 370 | Fill db table 'Topic' with topics from the graph, if new | 359 | Fill db table 'Topic' with topics from the graph, if new |
| 371 | ''' | 360 | ''' |
| 372 | - with Session(self._engine) as session: | 361 | + with Session(self._engine, future=True) as session: |
| 373 | db_topics = session.execute(select(Topic.id)).scalars().all() | 362 | db_topics = session.execute(select(Topic.id)).scalars().all() |
| 374 | new = [Topic(id=t) for t in topics if t not in db_topics] | 363 | new = [Topic(id=t) for t in topics if t not in db_topics] |
| 375 | if new: | 364 | if new: |
| @@ -389,11 +378,16 @@ class LearnApp(): | @@ -389,11 +378,16 @@ class LearnApp(): | ||
| 389 | 'Use "initdb-aprendizations" to create') | 378 | 'Use "initdb-aprendizations" to create') |
| 390 | 379 | ||
| 391 | self._engine = create_engine(f'sqlite:///{database}', future=True) | 380 | self._engine = create_engine(f'sqlite:///{database}', future=True) |
| 381 | + | ||
| 382 | + | ||
| 392 | try: | 383 | try: |
| 393 | - with Session(self._engine) as session: | ||
| 394 | - count_students: int = session.query(Student).count() | ||
| 395 | - count_topics: int = session.query(Topic).count() | ||
| 396 | - count_answers: int = session.query(Answer).count() | 384 | + query_students = select(func.count(Student.id)) |
| 385 | + query_topics = select(func.count(Topic.id)) | ||
| 386 | + query_answers = select(func.count(Answer.id)) | ||
| 387 | + with Session(self._engine, future=True) as session: | ||
| 388 | + count_students = session.execute(query_students).scalar() | ||
| 389 | + count_topics = session.execute(query_topics).scalar() | ||
| 390 | + count_answers = session.execute(query_answers).scalar() | ||
| 397 | except Exception as exc: | 391 | except Exception as exc: |
| 398 | logger.error('Database "%s" not usable!', database) | 392 | logger.error('Database "%s" not usable!', database) |
| 399 | raise DatabaseUnusableError() from exc | 393 | raise DatabaseUnusableError() from exc |
| @@ -615,29 +609,27 @@ class LearnApp(): | @@ -615,29 +609,27 @@ class LearnApp(): | ||
| 615 | the rankings. This is so that there can be users for development or | 609 | the rankings. This is so that there can be users for development or |
| 616 | testing purposes, which are not real users. | 610 | testing purposes, which are not real users. |
| 617 | The user_id of real students must have >2 chars. | 611 | The user_id of real students must have >2 chars. |
| 612 | + This should be modified to have a "visible" flag | ||
| 618 | ''' | 613 | ''' |
| 619 | 614 | ||
| 620 | logger.info('User "%s" get rankings for %s', uid, course_id) | 615 | logger.info('User "%s" get rankings for %s', uid, course_id) |
| 621 | - with self._db_session() as sess: | 616 | + query_students = select(Student.id, Student.name) |
| 617 | + query_student_topics = select(StudentTopic.student_id, | ||
| 618 | + StudentTopic.topic_id, | ||
| 619 | + StudentTopic.level, | ||
| 620 | + StudentTopic.date) | ||
| 621 | + query_total = select(Answer.student_id, func.count(Answer.ref)) | ||
| 622 | + query_right = select(Answer.student_id, func.count(Answer.ref)).where(Answer.grade == 1.0) | ||
| 623 | + with Session(self._engine, future=True) as session: | ||
| 622 | # all students in the database FIXME only with answers of this course | 624 | # all students in the database FIXME only with answers of this course |
| 623 | - students = sess.query(Student.id, Student.name).all() | 625 | + students = session.execute(query_students).all() |
| 624 | 626 | ||
| 625 | # topic levels FIXME only topics of this course | 627 | # topic levels FIXME only topics of this course |
| 626 | - student_topics = sess.query(StudentTopic.student_id, | ||
| 627 | - StudentTopic.topic_id, | ||
| 628 | - StudentTopic.level, | ||
| 629 | - StudentTopic.date).all() | 628 | + student_topics = session.execute(query_student_topics).all() |
| 630 | 629 | ||
| 631 | # answer performance | 630 | # answer performance |
| 632 | - total = dict(sess.query(Answer.student_id, | ||
| 633 | - sa.func.count(Answer.ref)) \ | ||
| 634 | - .group_by(Answer.student_id) \ | ||
| 635 | - .all()) | ||
| 636 | - right = dict(sess.query(Answer.student_id, | ||
| 637 | - sa.func.count(Answer.ref)) \ | ||
| 638 | - .filter(Answer.grade == 1.0) \ | ||
| 639 | - .group_by(Answer.student_id) \ | ||
| 640 | - .all()) | 631 | + total = dict(session.execute(query_total).all()) |
| 632 | + right = dict(session.execute(query_right).all()) | ||
| 641 | 633 | ||
| 642 | # compute percentage of right answers | 634 | # compute percentage of right answers |
| 643 | perf: Dict[str, float] = {u: right.get(u, 0.0) / total[u] | 635 | perf: Dict[str, float] = {u: right.get(u, 0.0) / total[u] |
demo/math/multiplication/multiplication-table.py
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | ||
| 3 | import random | 3 | import random |
| 4 | -import sys | ||
| 5 | 4 | ||
| 6 | -# can be repeated | ||
| 7 | -# x = random.randint(2, 9) | ||
| 8 | -# y = random.randint(2, 9) | ||
| 9 | - | ||
| 10 | -# with x != y | ||
| 11 | x, y = random.sample(range(2,10), k=2) | 5 | x, y = random.sample(range(2,10), k=2) |
| 12 | r = x * y | 6 | r = x * y |
| 13 | 7 | ||
| @@ -19,6 +13,7 @@ type: text | @@ -19,6 +13,7 @@ type: text | ||
| 19 | title: Multiplicação (tabuada) | 13 | title: Multiplicação (tabuada) |
| 20 | text: | | 14 | text: | |
| 21 | Qual o resultado da multiplicação ${x}\\times {y}$? | 15 | Qual o resultado da multiplicação ${x}\\times {y}$? |
| 16 | +transform: ['trim'] | ||
| 22 | correct: ['{r}'] | 17 | correct: ['{r}'] |
| 23 | solution: | | 18 | solution: | |
| 24 | A multiplicação é a repetição da soma. Podemos fazer de duas maneiras: | 19 | A multiplicação é a repetição da soma. Podemos fazer de duas maneiras: |
demo/math/multiplication/questions.yaml
| 1 | --- | 1 | --- |
| 2 | -# --------------------------------------------------------------------------- | 2 | +# ---------------------------------------------------------------------------- |
| 3 | - type: generator | 3 | - type: generator |
| 4 | ref: multiplication-table | 4 | ref: multiplication-table |
| 5 | script: multiplication-table.py | 5 | script: multiplication-table.py |
| 6 | 6 | ||
| 7 | +# ---------------------------------------------------------------------------- | ||
| 7 | - type: checkbox | 8 | - type: checkbox |
| 8 | ref: multiplication-properties | 9 | ref: multiplication-properties |
| 9 | title: Propriedades da multiplicação | 10 | title: Propriedades da multiplicação |
| @@ -17,7 +18,7 @@ | @@ -17,7 +18,7 @@ | ||
| 17 | # wrong | 18 | # wrong |
| 18 | - Existência de inverso, todos os números $x$ tem um inverso $1/x$ tal que | 19 | - Existência de inverso, todos os números $x$ tem um inverso $1/x$ tal que |
| 19 | $x(1/x)=1$. | 20 | $x(1/x)=1$. |
| 20 | - correct: [1, 1, 1, 1, -1] | 21 | + correct: [1, 1, 1, 1, 0] |
| 21 | solution: | | 22 | solution: | |
| 22 | Na multiplicação nem todos os números têm inverso. Só têm inverso os números | 23 | Na multiplicação nem todos os números têm inverso. Só têm inverso os números |
| 23 | diferentes de zero. | 24 | diferentes de zero. |