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 | 134 | print(f'Database: {args.db}') |
| 135 | 135 | engine = create_engine(f'sqlite:///{args.db}', echo=False, future=True) |
| 136 | 136 | Base.metadata.create_all(engine) # Creates schema if needed |
| 137 | - session = Session(engine) | |
| 137 | + session = Session(engine, future=True) | |
| 138 | 138 | |
| 139 | 139 | # --- build list of students to insert/update |
| 140 | 140 | students = [] | ... | ... |
aprendizations/learnapp.py
| ... | ... | @@ -16,8 +16,9 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict |
| 16 | 16 | # third party libraries |
| 17 | 17 | import bcrypt |
| 18 | 18 | import networkx as nx |
| 19 | -from sqlalchemy import create_engine, select | |
| 19 | +from sqlalchemy import create_engine, select, func | |
| 20 | 20 | from sqlalchemy.orm import Session |
| 21 | +from sqlalchemy.exc import NoResultFound | |
| 21 | 22 | |
| 22 | 23 | # this project |
| 23 | 24 | from aprendizations.models import Student, Answer, Topic, StudentTopic |
| ... | ... | @@ -55,25 +56,6 @@ class LearnApp(): |
| 55 | 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 | 60 | def __init__(self, |
| 79 | 61 | courses: str, # filename with course configurations |
| ... | ... | @@ -135,7 +117,7 @@ class LearnApp(): |
| 135 | 117 | # ------------------------------------------------------------------------ |
| 136 | 118 | def _sanity_check_questions(self) -> None: |
| 137 | 119 | ''' |
| 138 | - Unity tests for all questions | |
| 120 | + Unit tests for all questions | |
| 139 | 121 | |
| 140 | 122 | Generates all questions, give right and wrong answers and corrects. |
| 141 | 123 | ''' |
| ... | ... | @@ -173,7 +155,7 @@ class LearnApp(): |
| 173 | 155 | continue # to next test |
| 174 | 156 | |
| 175 | 157 | if errors > 0: |
| 176 | - logger.error('%6d error(s) found.', errors) # {errors:>6} | |
| 158 | + logger.error('%6d error(s) found.', errors) | |
| 177 | 159 | raise LearnException('Sanity checks') |
| 178 | 160 | logger.info(' 0 errors found.') |
| 179 | 161 | |
| ... | ... | @@ -181,26 +163,22 @@ class LearnApp(): |
| 181 | 163 | async def login(self, uid: str, password: str) -> bool: |
| 182 | 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 | 166 | # wait random time to minimize timing attacks |
| 190 | 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 | 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 | 175 | return False |
| 198 | 176 | |
| 199 | - name, hashed_pw = found | |
| 177 | + loop = asyncio.get_running_loop() | |
| 200 | 178 | pw_ok: bool = await loop.run_in_executor(None, |
| 201 | 179 | bcrypt.checkpw, |
| 202 | 180 | password.encode('utf-8'), |
| 203 | - hashed_pw) | |
| 181 | + student.password) | |
| 204 | 182 | |
| 205 | 183 | if pw_ok: |
| 206 | 184 | if uid in self.online: |
| ... | ... | @@ -210,10 +188,10 @@ class LearnApp(): |
| 210 | 188 | logger.info('User "%s" logged in', uid) |
| 211 | 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 | 196 | state = {t.topic_id: { |
| 219 | 197 | 'level': t.level, |
| ... | ... | @@ -222,7 +200,7 @@ class LearnApp(): |
| 222 | 200 | |
| 223 | 201 | self.online[uid] = { |
| 224 | 202 | 'number': uid, |
| 225 | - 'name': name, | |
| 203 | + 'name': student.name, | |
| 226 | 204 | 'state': StudentState(uid=uid, state=state, |
| 227 | 205 | courses=self.courses, deps=self.deps, |
| 228 | 206 | factory=self.factory), |
| ... | ... | @@ -250,14 +228,16 @@ class LearnApp(): |
| 250 | 228 | return False |
| 251 | 229 | |
| 252 | 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 | 242 | logger.info('User "%s" changed password', uid) |
| 263 | 243 | return True |
| ... | ... | @@ -279,13 +259,15 @@ class LearnApp(): |
| 279 | 259 | logger.info('User "%s" got %.2f in "%s"', uid, grade, ref) |
| 280 | 260 | |
| 281 | 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 | 272 | return question |
| 291 | 273 | |
| ... | ... | @@ -295,38 +277,45 @@ class LearnApp(): |
| 295 | 277 | Get the question to show (current or new one) |
| 296 | 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 | 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 | 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 | 298 | if student_topic is None: |
| 315 | 299 | # insert new studenttopic into database |
| 316 | 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 | 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 | 311 | else: |
| 324 | 312 | # update studenttopic in database |
| 325 | 313 | logger.debug('db update studenttopic to level %f', level) |
| 326 | 314 | student_topic.level = level |
| 327 | 315 | student_topic.date = date |
| 328 | 316 | |
| 329 | - sess.add(student_topic) | |
| 317 | + session.add(student_topic) | |
| 318 | + session.commit() | |
| 330 | 319 | |
| 331 | 320 | return question |
| 332 | 321 | |
| ... | ... | @@ -334,9 +323,9 @@ class LearnApp(): |
| 334 | 323 | def start_course(self, uid: str, course_id: str) -> None: |
| 335 | 324 | '''Start course''' |
| 336 | 325 | |
| 337 | - student = self.online[uid]['state'] | |
| 326 | + student_state = self.online[uid]['state'] | |
| 338 | 327 | try: |
| 339 | - student.start_course(course_id) | |
| 328 | + student_state.start_course(course_id) | |
| 340 | 329 | except Exception as exc: |
| 341 | 330 | logger.warning('"%s" could not start course "%s"', uid, course_id) |
| 342 | 331 | raise LearnException() from exc |
| ... | ... | @@ -369,7 +358,7 @@ class LearnApp(): |
| 369 | 358 | ''' |
| 370 | 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 | 362 | db_topics = session.execute(select(Topic.id)).scalars().all() |
| 374 | 363 | new = [Topic(id=t) for t in topics if t not in db_topics] |
| 375 | 364 | if new: |
| ... | ... | @@ -389,11 +378,16 @@ class LearnApp(): |
| 389 | 378 | 'Use "initdb-aprendizations" to create') |
| 390 | 379 | |
| 391 | 380 | self._engine = create_engine(f'sqlite:///{database}', future=True) |
| 381 | + | |
| 382 | + | |
| 392 | 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 | 391 | except Exception as exc: |
| 398 | 392 | logger.error('Database "%s" not usable!', database) |
| 399 | 393 | raise DatabaseUnusableError() from exc |
| ... | ... | @@ -615,29 +609,27 @@ class LearnApp(): |
| 615 | 609 | the rankings. This is so that there can be users for development or |
| 616 | 610 | testing purposes, which are not real users. |
| 617 | 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 | 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 | 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 | 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 | 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 | 634 | # compute percentage of right answers |
| 643 | 635 | perf: Dict[str, float] = {u: right.get(u, 0.0) / total[u] | ... | ... |
demo/math/multiplication/multiplication-table.py
| 1 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | |
| 3 | 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 | 5 | x, y = random.sample(range(2,10), k=2) |
| 12 | 6 | r = x * y |
| 13 | 7 | |
| ... | ... | @@ -19,6 +13,7 @@ type: text |
| 19 | 13 | title: Multiplicação (tabuada) |
| 20 | 14 | text: | |
| 21 | 15 | Qual o resultado da multiplicação ${x}\\times {y}$? |
| 16 | +transform: ['trim'] | |
| 22 | 17 | correct: ['{r}'] |
| 23 | 18 | solution: | |
| 24 | 19 | A multiplicação é a repetição da soma. Podemos fazer de duas maneiras: | ... | ... |
demo/math/multiplication/questions.yaml
| 1 | 1 | --- |
| 2 | -# --------------------------------------------------------------------------- | |
| 2 | +# ---------------------------------------------------------------------------- | |
| 3 | 3 | - type: generator |
| 4 | 4 | ref: multiplication-table |
| 5 | 5 | script: multiplication-table.py |
| 6 | 6 | |
| 7 | +# ---------------------------------------------------------------------------- | |
| 7 | 8 | - type: checkbox |
| 8 | 9 | ref: multiplication-properties |
| 9 | 10 | title: Propriedades da multiplicação |
| ... | ... | @@ -17,7 +18,7 @@ |
| 17 | 18 | # wrong |
| 18 | 19 | - Existência de inverso, todos os números $x$ tem um inverso $1/x$ tal que |
| 19 | 20 | $x(1/x)=1$. |
| 20 | - correct: [1, 1, 1, 1, -1] | |
| 21 | + correct: [1, 1, 1, 1, 0] | |
| 21 | 22 | solution: | |
| 22 | 23 | Na multiplicação nem todos os números têm inverso. Só têm inverso os números |
| 23 | 24 | diferentes de zero. | ... | ... |