Commit 547f01489590e33dc6131514352b7653d04309c5

Authored by Miguel Barão
1 parent 97085de7
Exists in master and in 1 other branch dev

finished convertion to sqlalchemy 1.4/2.0

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.