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 | # BUGS | 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 | - show-ref nao esta a funcionar na correccao (pelo menos) | 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 | - JOBE correct async | 8 | - JOBE correct async |
11 | - 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. | 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 | - QuestionCode falta reportar nos comments os vários erros que podem ocorrer (timeout, etc) | 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 | - grade gives internal server error?? | 11 | - grade gives internal server error?? |
15 | - reload do teste recomeça a contagem no inicio do tempo. | 12 | - reload do teste recomeça a contagem no inicio do tempo. |
16 | - 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. | 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,6 +21,8 @@ | ||
24 | 21 | ||
25 | # TODO | 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 | - stress tests. use https://locust.io | 26 | - stress tests. use https://locust.io |
28 | - wait for admin to start test. (students can be allowed earlier) | 27 | - wait for admin to start test. (students can be allowed earlier) |
29 | - impedir os eventos copy/paste. alunos usam isso para trazer codigo ja feito nos computadores. Obrigar a fazer reset? fazer um copy automaticamente? | 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,6 +71,9 @@ ou usar push (websockets?) | ||
72 | 71 | ||
73 | # FIXED | 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 | - mostrar unfocus e window area em /admin | 77 | - mostrar unfocus e window area em /admin |
76 | - CRITICAL se answer for `i<n` a revisão de provas mostra apenas i (interpreta `<` como tag?) | 78 | - CRITICAL se answer for `i<n` a revisão de provas mostra apenas i (interpreta `<` como tag?) |
77 | - botao de autorizar desliga-se, fazer debounce. | 79 | - botao de autorizar desliga-se, fazer debounce. |
perguntations/app.py
@@ -258,61 +258,66 @@ class App(): | @@ -258,61 +258,66 @@ class App(): | ||
258 | fname = '--'.join(fields) + '.json' | 258 | fname = '--'.join(fields) + '.json' |
259 | fpath = path.join(test['answers_dir'], fname) | 259 | fpath = path.join(test['answers_dir'], fname) |
260 | with open(path.expanduser(fpath), 'w') as file: | 260 | with open(path.expanduser(fpath), 'w') as file: |
261 | - # default=str required for datetime objects | ||
262 | json.dump(test, file, indent=2, default=str) | 261 | json.dump(test, file, indent=2, default=str) |
262 | + # option default=str is required for datetime objects | ||
263 | + | ||
263 | logger.info('"%s" saved JSON.', uid) | 264 | logger.info('"%s" saved JSON.', uid) |
264 | 265 | ||
265 | # --- insert test and questions into database | 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 | with self._db_session() as sess: | 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 | logger.info('"%s" database updated.', uid) | 291 | logger.info('"%s" database updated.', uid) |
287 | return grade | 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 | def event_test(self, uid, cmd, value): | 323 | def event_test(self, uid, cmd, value): |
@@ -334,55 +339,56 @@ class App(): | @@ -334,55 +339,56 @@ class App(): | ||
334 | 339 | ||
335 | def get_questions_csv(self): | 340 | def get_questions_csv(self): |
336 | '''generates a CSV with the grades of the test''' | 341 | '''generates a CSV with the grades of the test''' |
337 | - test_id = self.testfactory['ref'] | ||
338 | - | 342 | + test_ref = self.testfactory['ref'] |
339 | with self._db_session() as sess: | 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 | csvstr = io.StringIO() | 364 | csvstr = io.StringIO() |
365 | writer = csv.DictWriter(csvstr, fieldnames=cols, restval=None, | 365 | writer = csv.DictWriter(csvstr, fieldnames=cols, restval=None, |
366 | delimiter=';', quoting=csv.QUOTE_ALL) | 366 | delimiter=';', quoting=csv.QUOTE_ALL) |
367 | writer.writeheader() | 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 | def get_test_csv(self): | 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 | with self._db_session() as sess: | 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 | Test.starttime, Test.finishtime)\ | 377 | Test.starttime, Test.finishtime)\ |
377 | - .filter(Test.ref == self.testfactory['ref'])\ | 378 | + .filter(Test.ref == test_ref)\ |
378 | .order_by(Test.student_id)\ | 379 | .order_by(Test.student_id)\ |
379 | .all() | 380 | .all() |
380 | 381 | ||
382 | + if not tests: | ||
383 | + logger.warning('Empty CSV: there are no tests!') | ||
384 | + return test_ref, '' | ||
385 | + | ||
381 | csvstr = io.StringIO() | 386 | csvstr = io.StringIO() |
382 | writer = csv.writer(csvstr, delimiter=';', quoting=csv.QUOTE_ALL) | 387 | writer = csv.writer(csvstr, delimiter=';', quoting=csv.QUOTE_ALL) |
383 | writer.writerow(('Aluno', 'Nota', 'Início', 'Fim')) | 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 | def get_student_grades_from_all_tests(self, uid): | 394 | def get_student_grades_from_all_tests(self, uid): |
perguntations/initdb.py
@@ -19,7 +19,7 @@ import sqlalchemy as sa | @@ -19,7 +19,7 @@ import sqlalchemy as sa | ||
19 | from perguntations.models import Base, Student | 19 | from perguntations.models import Base, Student |
20 | 20 | ||
21 | 21 | ||
22 | -# =========================================================================== | 22 | +# ============================================================================ |
23 | def parse_commandline_arguments(): | 23 | def parse_commandline_arguments(): |
24 | '''Parse command line options''' | 24 | '''Parse command line options''' |
25 | parser = argparse.ArgumentParser( | 25 | parser = argparse.ArgumentParser( |
@@ -68,7 +68,7 @@ def parse_commandline_arguments(): | @@ -68,7 +68,7 @@ def parse_commandline_arguments(): | ||
68 | return parser.parse_args() | 68 | return parser.parse_args() |
69 | 69 | ||
70 | 70 | ||
71 | -# =========================================================================== | 71 | +# ============================================================================ |
72 | def get_students_from_csv(filename): | 72 | def get_students_from_csv(filename): |
73 | ''' | 73 | ''' |
74 | SIIUE names have alien strings like "(TE)" and are sometimes capitalized | 74 | SIIUE names have alien strings like "(TE)" and are sometimes capitalized |
@@ -97,7 +97,7 @@ def get_students_from_csv(filename): | @@ -97,7 +97,7 @@ def get_students_from_csv(filename): | ||
97 | return students | 97 | return students |
98 | 98 | ||
99 | 99 | ||
100 | -# =========================================================================== | 100 | +# ============================================================================ |
101 | def hashpw(student, password=None): | 101 | def hashpw(student, password=None): |
102 | '''replace password by hash for a single student''' | 102 | '''replace password by hash for a single student''' |
103 | print('.', end='', flush=True) | 103 | print('.', end='', flush=True) |
@@ -108,7 +108,7 @@ def hashpw(student, password=None): | @@ -108,7 +108,7 @@ def hashpw(student, password=None): | ||
108 | bcrypt.gensalt()) | 108 | bcrypt.gensalt()) |
109 | 109 | ||
110 | 110 | ||
111 | -# =========================================================================== | 111 | +# ============================================================================ |
112 | def insert_students_into_db(session, students): | 112 | def insert_students_into_db(session, students): |
113 | '''insert list of students into the database''' | 113 | '''insert list of students into the database''' |
114 | try: | 114 | try: |
perguntations/models.py
@@ -25,9 +25,8 @@ class Student(Base): | @@ -25,9 +25,8 @@ class Student(Base): | ||
25 | 25 | ||
26 | # --- | 26 | # --- |
27 | tests = relationship('Test', back_populates='student') | 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 | return (f'Student:\n' | 30 | return (f'Student:\n' |
32 | f' id: "{self.id}"\n' | 31 | f' id: "{self.id}"\n' |
33 | f' name: "{self.name}"\n' | 32 | f' name: "{self.name}"\n' |
@@ -53,12 +52,12 @@ class Test(Base): | @@ -53,12 +52,12 @@ class Test(Base): | ||
53 | student = relationship('Student', back_populates='tests') | 52 | student = relationship('Student', back_populates='tests') |
54 | questions = relationship('Question', back_populates='test') | 53 | questions = relationship('Question', back_populates='test') |
55 | 54 | ||
56 | - def __repr__(self): | 55 | + def __str__(self): |
57 | return (f'Test:\n' | 56 | return (f'Test:\n' |
58 | - f' id: "{self.id}"\n' | 57 | + f' id: {self.id}\n' |
59 | f' ref: "{self.ref}"\n' | 58 | f' ref: "{self.ref}"\n' |
60 | f' title: "{self.title}"\n' | 59 | f' title: "{self.title}"\n' |
61 | - f' grade: "{self.grade}"\n' | 60 | + f' grade: {self.grade}\n' |
62 | f' state: "{self.state}"\n' | 61 | f' state: "{self.state}"\n' |
63 | f' comment: "{self.comment}"\n' | 62 | f' comment: "{self.comment}"\n' |
64 | f' starttime: "{self.starttime}"\n' | 63 | f' starttime: "{self.starttime}"\n' |
@@ -72,23 +71,24 @@ class Question(Base): | @@ -72,23 +71,24 @@ class Question(Base): | ||
72 | '''Question table''' | 71 | '''Question table''' |
73 | __tablename__ = 'questions' | 72 | __tablename__ = 'questions' |
74 | id = Column(Integer, primary_key=True) # auto_increment | 73 | id = Column(Integer, primary_key=True) # auto_increment |
74 | + number = Column(Integer) # question number (ref may be not be unique) | ||
75 | ref = Column(String) | 75 | ref = Column(String) |
76 | grade = Column(Float) | 76 | grade = Column(Float) |
77 | + comment = Column(String) | ||
77 | starttime = Column(String) | 78 | starttime = Column(String) |
78 | finishtime = Column(String) | 79 | finishtime = Column(String) |
79 | - student_id = Column(String, ForeignKey('students.id')) | ||
80 | test_id = Column(String, ForeignKey('tests.id')) | 80 | test_id = Column(String, ForeignKey('tests.id')) |
81 | 81 | ||
82 | # --- | 82 | # --- |
83 | - student = relationship('Student', back_populates='questions') | ||
84 | test = relationship('Test', back_populates='questions') | 83 | test = relationship('Test', back_populates='questions') |
85 | 84 | ||
86 | - def __repr__(self): | 85 | + def __str__(self): |
87 | return (f'Question:\n' | 86 | return (f'Question:\n' |
88 | - f' id: "{self.id}"\n' | 87 | + f' id: {self.id}\n' |
88 | + f' number: {self.number}\n' | ||
89 | f' ref: "{self.ref}"\n' | 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 | f' starttime: "{self.starttime}"\n' | 92 | f' starttime: "{self.starttime}"\n' |
92 | f' finishtime: "{self.finishtime}"\n' | 93 | f' finishtime: "{self.finishtime}"\n' |
93 | - f' student_id: "{self.student_id}"\n' # FIXME normal form | ||
94 | f' test_id: "{self.test_id}"\n') | 94 | f' test_id: "{self.test_id}"\n') |