Commit bb58d2f441be7cf88c79ba5b20ba20ad585cfa7a
1 parent
aab6a036
Exists in
master
and in
1 other branch
- 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.
Showing
4 changed files
with
107 additions
and
99 deletions
Show diff stats
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') | ... | ... |