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 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.
... ...