Commit bb58d2f441be7cf88c79ba5b20ba20ad585cfa7a

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

- changes how database is updated after correction of a test

- fix get_questions_csv when there are questions with duplicate refs
- modified database model to include question number and comment in the
questions table. Also removed student_id so that it is now normalized.
BUGS.md
1 1  
2 2 # BUGS
3 3  
4   -- perguntas repetidas (mesma ref) dao asneira, porque a referencia é usada como chave em varios sitios e as chaves nao podem ser dupplicadas.
5   - da asneira pelo menos na funcao get_questions_csv. na base de dados tem de estar registado tb o numero da pergunta, caso contrario é impossível saber a qual corresponde.
  4 +- esta a corrigir código JOBE mesmo que nao tenha respondido???
6 5 - show-ref nao esta a funcionar na correccao (pelo menos)
7   -- algumas submissões ficaram em duplicado na base de dados. a base de dados deveria ter como chave do teste um id que fosse único desse teste particular (não um auto counter, nem ref do teste)
8   -- guardar nota final grade truncado em zero e sem ser truncado (quando é necessário fazer correcções à mão às perguntas, é necessário o valor não truncado)
9   -- internal server error quando em --review, download csv detalhado.
  6 +- algumas vezes a base de dados guarda o mesmo teste em duplicado. ver se dois submits dao origem a duas correcções.
  7 +talvez a base de dados devesse ter como chave do teste um id que fosse único desse teste particular (não um auto counter, nem ref do teste)
10 8 - JOBE correct async
11 9 - em caso de timeout na submissão (e.g. JOBE ou script nao responde) a correcção não termina e o teste não é guardado.
12 10 - QuestionCode falta reportar nos comments os vários erros que podem ocorrer (timeout, etc)
13   -- permitir remover alunos que estão online para poderem comecar de novo.
14 11 - grade gives internal server error??
15 12 - reload do teste recomeça a contagem no inicio do tempo.
16 13 - em admin, quando scale_max não é 20, as cores das barras continuam a reflectir a escala 0,20. a tabela teste na DB não tem a escala desse teste.
... ... @@ -24,6 +21,8 @@
24 21  
25 22 # TODO
26 23  
  24 +- permitir remover alunos que estão online para poderem comecar de novo.
  25 +- guardar nota final grade truncado em zero e sem ser truncado (quando é necessário fazer correcções à mão às perguntas, é necessário o valor não truncado)
27 26 - stress tests. use https://locust.io
28 27 - wait for admin to start test. (students can be allowed earlier)
29 28 - impedir os eventos copy/paste. alunos usam isso para trazer codigo ja feito nos computadores. Obrigar a fazer reset? fazer um copy automaticamente?
... ... @@ -72,6 +71,9 @@ ou usar push (websockets?)
72 71  
73 72 # FIXED
74 73  
  74 +- internal server error quando em --review, download csv detalhado.
  75 +- perguntas repetidas (mesma ref) dao asneira, porque a referencia é usada como chave em varios sitios e as chaves nao podem ser dupplicadas.
  76 + da asneira pelo menos na funcao get_questions_csv. na base de dados tem de estar registado tb o numero da pergunta, caso contrario é impossível saber a qual corresponde.
75 77 - mostrar unfocus e window area em /admin
76 78 - CRITICAL se answer for `i<n` a revisão de provas mostra apenas i (interpreta `<` como tag?)
77 79 - botao de autorizar desliga-se, fazer debounce.
... ...
perguntations/app.py
... ... @@ -258,61 +258,66 @@ class App():
258 258 fname = '--'.join(fields) + '.json'
259 259 fpath = path.join(test['answers_dir'], fname)
260 260 with open(path.expanduser(fpath), 'w') as file:
261   - # default=str required for datetime objects
262 261 json.dump(test, file, indent=2, default=str)
  262 + # option default=str is required for datetime objects
  263 +
263 264 logger.info('"%s" saved JSON.', uid)
264 265  
265 266 # --- insert test and questions into database
  267 + test_row = Test(
  268 + ref=test['ref'],
  269 + title=test['title'],
  270 + grade=test['grade'],
  271 + state=test['state'],
  272 + comment='',
  273 + starttime=str(test['start_time']),
  274 + finishtime=str(test['finish_time']),
  275 + filename=fpath,
  276 + student_id=uid)
  277 + test_row.questions = [Question(
  278 + number=n,
  279 + ref=q['ref'],
  280 + grade=q['grade'],
  281 + comment=q.get('comment', ''),
  282 + starttime=str(test['start_time']),
  283 + finishtime=str(test['finish_time']),
  284 + test_id=test['ref'])
  285 + for n, q in enumerate(test['questions'])
  286 + if 'grade' in q
  287 + ]
266 288 with self._db_session() as sess:
267   - sess.add(Test(
268   - ref=test['ref'],
269   - title=test['title'],
270   - grade=test['grade'],
271   - starttime=str(test['start_time']),
272   - finishtime=str(test['finish_time']),
273   - filename=fpath,
274   - student_id=uid,
275   - state=test['state'],
276   - comment=''))
277   - sess.add_all([Question(
278   - ref=q['ref'],
279   - grade=q['grade'],
280   - starttime=str(test['start_time']),
281   - finishtime=str(test['finish_time']),
282   - student_id=uid,
283   - test_id=test['ref'])
284   - for q in test['questions'] if 'grade' in q])
  289 + sess.add(test_row)
285 290  
286 291 logger.info('"%s" database updated.', uid)
287 292 return grade
288 293  
289 294 # ------------------------------------------------------------------------
290   - def giveup_test(self, uid):
291   - '''giveup test - not used??'''
292   - test = self.online[uid]['test']
293   - test.giveup()
294   -
295   - # save JSON with the test
296   - fields = (test['student']['number'], test['ref'],
297   - str(test['finish_time']))
298   - fname = '--'.join(fields) + '.json'
299   - fpath = path.join(test['answers_dir'], fname)
300   - test.save_json(fpath)
301   -
302   - # insert test into database
303   - with self._db_session() as sess:
304   - sess.add(Test(ref=test['ref'],
305   - title=test['title'],
306   - grade=test['grade'],
307   - starttime=str(test['start_time']),
308   - finishtime=str(test['finish_time']),
309   - filename=fpath,
310   - student_id=test['student']['number'],
311   - state=test['state'],
312   - comment=''))
313   -
314   - logger.info('"%s" gave up.', uid)
315   - return test
  295 + # def giveup_test(self, uid):
  296 + # '''giveup test - not used??'''
  297 + # test = self.online[uid]['test']
  298 + # test.giveup()
  299 +
  300 + # # save JSON with the test
  301 + # fields = (test['student']['number'], test['ref'],
  302 + # str(test['finish_time']))
  303 + # fname = '--'.join(fields) + '.json'
  304 + # fpath = path.join(test['answers_dir'], fname)
  305 + # test.save_json(fpath)
  306 +
  307 + # # insert test into database
  308 + # with self._db_session() as sess:
  309 + # sess.add(Test(ref=test['ref'],
  310 + # title=test['title'],
  311 + # grade=test['grade'],
  312 + # starttime=str(test['start_time']),
  313 + # finishtime=str(test['finish_time']),
  314 + # filename=fpath,
  315 + # # student_id=test['student']['number'],
  316 + # state=test['state'],
  317 + # comment=''))
  318 +
  319 + # logger.info('"%s" gave up.', uid)
  320 + # return test
316 321  
317 322 # ------------------------------------------------------------------------
318 323 def event_test(self, uid, cmd, value):
... ... @@ -334,55 +339,56 @@ class App():
334 339  
335 340 def get_questions_csv(self):
336 341 '''generates a CSV with the grades of the test'''
337   - test_id = self.testfactory['ref']
338   -
  342 + test_ref = self.testfactory['ref']
339 343 with self._db_session() as sess:
340   - grades = sess.query(Question.student_id, Question.starttime,
341   - Question.ref, Question.grade)\
342   - .filter(Question.test_id == test_id)\
343   - .order_by(Question.student_id)\
344   - .all()
345   -
346   - # first row of the csv will include student_id, date/time, and refs for
347   - # all questions declared in the test
348   - cols = ['Aluno', 'Início'] + \
349   - [r for question in self.testfactory['questions']
350   - for r in question['ref']]
351   -
352   - # tests is a dict of dicts: FIXME what about duplicate qrefs???
353   - # { (student_id, time): {qref: grade, ...} }
354   - # the key is a tuple (student_id, time) because the same student can
355   - # repeat the same test multiple times (if the professor allows).
356   - tests = {}
357   - for question in grades:
358   - student, qref, qgrade = question[:2], *question[2:]
359   - tests.setdefault(student, {})[qref] = qgrade
360   -
361   - rows = [{'Aluno': test[0], 'Início': test[1], **q}
362   - for test, q in tests.items()]
  344 + questions = sess.query(Test.id, Test.student_id, Test.starttime,
  345 + Question.number, Question.grade)\
  346 + .join(Question)\
  347 + .filter(Test.ref == test_ref)\
  348 + .all()
  349 +
  350 + qnums = set() # keeps track of all the questions in the test
  351 + tests = {} # {test_id: {student_id, starttime, 0: grade, ...}}
  352 + for question in questions:
  353 + test_id, student_id, starttime, num, grade = question
  354 + default_test_id = {'Aluno': student_id, 'Início': starttime}
  355 + tests.setdefault(test_id, default_test_id)[num] = grade
  356 + qnums.add(num)
  357 +
  358 + if not tests:
  359 + logger.warning('Empty CSV: there are no tests!')
  360 + return test_ref, ''
  361 +
  362 + cols = ['Aluno', 'Início'] + list(qnums)
363 363  
364 364 csvstr = io.StringIO()
365 365 writer = csv.DictWriter(csvstr, fieldnames=cols, restval=None,
366 366 delimiter=';', quoting=csv.QUOTE_ALL)
367 367 writer.writeheader()
368   - writer.writerows(rows)
369   - return test_id, csvstr.getvalue()
370   -
  368 + writer.writerows(tests.values())
  369 + return test_ref, csvstr.getvalue()
371 370  
372 371 def get_test_csv(self):
373   - '''generates a CSV with the grades of the test'''
  372 + '''generates a CSV with the grades of the test currently running'''
  373 + test_ref = self.testfactory['ref']
374 374 with self._db_session() as sess:
375   - grades = sess.query(Test.student_id, Test.grade,
  375 + tests = sess.query(Test.student_id,
  376 + Test.grade,
376 377 Test.starttime, Test.finishtime)\
377   - .filter(Test.ref == self.testfactory['ref'])\
  378 + .filter(Test.ref == test_ref)\
378 379 .order_by(Test.student_id)\
379 380 .all()
380 381  
  382 + if not tests:
  383 + logger.warning('Empty CSV: there are no tests!')
  384 + return test_ref, ''
  385 +
381 386 csvstr = io.StringIO()
382 387 writer = csv.writer(csvstr, delimiter=';', quoting=csv.QUOTE_ALL)
383 388 writer.writerow(('Aluno', 'Nota', 'Início', 'Fim'))
384   - writer.writerows(grades)
385   - return self.testfactory['ref'], csvstr.getvalue()
  389 + writer.writerows(tests)
  390 +
  391 + return test_ref, csvstr.getvalue()
386 392  
387 393 # ------------------------------------------------------------------------
388 394 def get_student_grades_from_all_tests(self, uid):
... ...
perguntations/initdb.py
... ... @@ -19,7 +19,7 @@ import sqlalchemy as sa
19 19 from perguntations.models import Base, Student
20 20  
21 21  
22   -# ===========================================================================
  22 +# ============================================================================
23 23 def parse_commandline_arguments():
24 24 '''Parse command line options'''
25 25 parser = argparse.ArgumentParser(
... ... @@ -68,7 +68,7 @@ def parse_commandline_arguments():
68 68 return parser.parse_args()
69 69  
70 70  
71   -# ===========================================================================
  71 +# ============================================================================
72 72 def get_students_from_csv(filename):
73 73 '''
74 74 SIIUE names have alien strings like "(TE)" and are sometimes capitalized
... ... @@ -97,7 +97,7 @@ def get_students_from_csv(filename):
97 97 return students
98 98  
99 99  
100   -# ===========================================================================
  100 +# ============================================================================
101 101 def hashpw(student, password=None):
102 102 '''replace password by hash for a single student'''
103 103 print('.', end='', flush=True)
... ... @@ -108,7 +108,7 @@ def hashpw(student, password=None):
108 108 bcrypt.gensalt())
109 109  
110 110  
111   -# ===========================================================================
  111 +# ============================================================================
112 112 def insert_students_into_db(session, students):
113 113 '''insert list of students into the database'''
114 114 try:
... ...
perguntations/models.py
... ... @@ -25,9 +25,8 @@ class Student(Base):
25 25  
26 26 # ---
27 27 tests = relationship('Test', back_populates='student')
28   - questions = relationship('Question', back_populates='student')
29 28  
30   - def __repr__(self):
  29 + def __str__(self):
31 30 return (f'Student:\n'
32 31 f' id: "{self.id}"\n'
33 32 f' name: "{self.name}"\n'
... ... @@ -53,12 +52,12 @@ class Test(Base):
53 52 student = relationship('Student', back_populates='tests')
54 53 questions = relationship('Question', back_populates='test')
55 54  
56   - def __repr__(self):
  55 + def __str__(self):
57 56 return (f'Test:\n'
58   - f' id: "{self.id}"\n'
  57 + f' id: {self.id}\n'
59 58 f' ref: "{self.ref}"\n'
60 59 f' title: "{self.title}"\n'
61   - f' grade: "{self.grade}"\n'
  60 + f' grade: {self.grade}\n'
62 61 f' state: "{self.state}"\n'
63 62 f' comment: "{self.comment}"\n'
64 63 f' starttime: "{self.starttime}"\n'
... ... @@ -72,23 +71,24 @@ class Question(Base):
72 71 '''Question table'''
73 72 __tablename__ = 'questions'
74 73 id = Column(Integer, primary_key=True) # auto_increment
  74 + number = Column(Integer) # question number (ref may be not be unique)
75 75 ref = Column(String)
76 76 grade = Column(Float)
  77 + comment = Column(String)
77 78 starttime = Column(String)
78 79 finishtime = Column(String)
79   - student_id = Column(String, ForeignKey('students.id'))
80 80 test_id = Column(String, ForeignKey('tests.id'))
81 81  
82 82 # ---
83   - student = relationship('Student', back_populates='questions')
84 83 test = relationship('Test', back_populates='questions')
85 84  
86   - def __repr__(self):
  85 + def __str__(self):
87 86 return (f'Question:\n'
88   - f' id: "{self.id}"\n'
  87 + f' id: {self.id}\n'
  88 + f' number: {self.number}\n'
89 89 f' ref: "{self.ref}"\n'
90   - f' grade: "{self.grade}"\n'
  90 + f' grade: {self.grade}\n'
  91 + f' comment: "{self.comment}"\n'
91 92 f' starttime: "{self.starttime}"\n'
92 93 f' finishtime: "{self.finishtime}"\n'
93   - f' student_id: "{self.student_id}"\n' # FIXME normal form
94 94 f' test_id: "{self.test_id}"\n')
... ...