Commit 526721fdac27a947ac5c001137c29fc6ba40f164
1 parent
c68097b2
Exists in
master
and in
1 other branch
- changed text-numeric to numeric-interval
- many changes and fixes. review still hardcoded...
Showing
12 changed files
with
236 additions
and
80 deletions
Show diff stats
BUGS.md
1 | 1 | |
2 | 2 | # BUGS |
3 | 3 | |
4 | -- testar perguntas warning/warn | |
5 | -- text-numeric não está a gerar a pergunta. faltam templates? | |
4 | +- numeracao das perguntas do teste esta a contar com paineis informativos... | |
5 | +- servir imagens das perguntas | |
6 | +- review de um teste nao funciona (hardcoded...) | |
7 | +- testar opcao --allow-all | |
8 | +- como alterar configuracao para mostrar logs de debug? | |
9 | +- hints nao funciona | |
6 | 10 | - uniformizar question.py com a de aprendizations... |
7 | 11 | - Review de um teste que foi apagado rebenta. |
8 | 12 | - permitir eliminar teste a decorrer de modo a que o aluno possa recomeçar (e.g. noutro browser) |
... | ... | @@ -11,13 +15,10 @@ |
11 | 15 | # TODO |
12 | 16 | |
13 | 17 | - Gerar pdf's com todos os testes no final (pdfkit). |
14 | -- testar SSL | |
15 | 18 | - manter registo dos unfocus durante o teste e de qual a pergunta visivel nesse momento |
16 | 19 | |
17 | 20 | - permitir varios testes, aluno escolhe qual o teste que quer fazer. |
18 | -- usar thread.Lock para aceder a variaveis de estado? | |
19 | 21 | - se ocorrer um erro na correcçao avisar aluno para contactar o professor. |
20 | -- implementar practice mode? | |
21 | 22 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova? |
22 | 23 | - detectar scroll e enviar posição para servidor (analise de scroll para detectar copianço? ou simplesmente para analisar como os alunos percorrem o teste) |
23 | 24 | - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). |
... | ... | @@ -30,6 +31,9 @@ |
30 | 31 | |
31 | 32 | # FIXED |
32 | 33 | |
34 | +- testar SSL | |
35 | +- text-numeric não está a gerar a pergunta. faltam templates? | |
36 | +- testar perguntas warning/warn | |
33 | 37 | - qd user 0 faz logout rebenta. |
34 | 38 | - Quando grava JSON do teste deve usar 'path' tal como definido na configuração e não expandido. Isto porque em OSX /home é /Users e quando se muda de um sistema para outro não encontra os testes. Assim, usando ~ na configuração deveria funcionar sempre. |
35 | 39 | - configuração do teste não joga bem com o do aprendizations. Em particular os scripts não ficam com o mesmo path!!! | ... | ... |
... | ... | @@ -0,0 +1,92 @@ |
1 | +- | |
2 | + ref: tut-information | |
3 | + type: information | |
4 | + title: information (ou info) | |
5 | + text: Texto informativo. Não conta para avaliação. | |
6 | +# --------------------------------------------------------------------------- | |
7 | +- | |
8 | + ref: tut-success | |
9 | + type: success | |
10 | + title: success | |
11 | + text: Texto de positivo (sucesso). Não conta para avaliação. | |
12 | +# --------------------------------------------------------------------------- | |
13 | +- | |
14 | + ref: tut-warning | |
15 | + type: warning | |
16 | + title: warning (ou warn) | |
17 | + text: Texto de aviso. Não conta para avaliação. | |
18 | +# ---------------------------------------------------------------------------- | |
19 | +- | |
20 | + ref: tut-alert | |
21 | + type: alert | |
22 | + title: alert | |
23 | + text: Texto negativo (alerta). Não conta para avaliação. | |
24 | +# ---------------------------------------------------------------------------- | |
25 | +- | |
26 | + ref: tut-radio | |
27 | + type: radio | |
28 | + title: radio | |
29 | + text: Escolha simples, apenas uma opção está correcta. | |
30 | + options: | |
31 | + - Opção 0 (correcta) | |
32 | + - Opção 1 | |
33 | + - Opção 2 | |
34 | + - Opção 3 | |
35 | + # opcionais e valores por defeito | |
36 | + shuffle: True | |
37 | + correct: 0 | |
38 | +# ---------------------------------------------------------------------------- | |
39 | +- | |
40 | + ref: tut-checkbox | |
41 | + type: checkbox | |
42 | + title: checkbox | |
43 | + text: Escolha simples, apenas uma opção está correcta. | |
44 | + options: | |
45 | + - Opção 0 (sim) | |
46 | + - Opção 1 (não) | |
47 | + - Opção 2 (não) | |
48 | + - Opção 3 (sim) | |
49 | + correct: [1,-1,-1,1] | |
50 | + # opcionais e valores por defeito | |
51 | + shuffle: True | |
52 | +# ---------------------------------------------------------------------------- | |
53 | +- | |
54 | + ref: tut-text | |
55 | + type: text | |
56 | + title: text | |
57 | + text: | | |
58 | + Resposta numa linha de texto. A resposta está correcta se coincidir com alguma das respostas admissíveis. | |
59 | + Neste exemplo a resposta correcta é `azul`, `Azul` ou `AZUL`. | |
60 | + correct: ['azul', 'Azul', 'AZUL'] | |
61 | +# --------------------------------------------------------------------------- | |
62 | +- | |
63 | + ref: tut-text-regex | |
64 | + type: text-regex | |
65 | + title: text-regex | |
66 | + text: | | |
67 | + Resposta numa linha de texto. A resposta é validada com uma expressão regular. | |
68 | + Neste exemplo a expressão regular é `(VERDE|[Vv]erde)`. | |
69 | + correct: !regex '(VERDE|[Vv]erde)' | |
70 | +# --------------------------------------------------------------------------- | |
71 | +- | |
72 | + ref: tut-numeric-interval | |
73 | + type: numeric-interval | |
74 | + title: numeric-interval | |
75 | + text: | | |
76 | + Resposta numérica numa linha de texto. A resposta é convertida para um float e tem de pertencer a um intervalo de valores. | |
77 | + Neste exemplo o intervalo é [3.14, 3.15]. | |
78 | + correct: [3.14, 3.15] | |
79 | +# --------------------------------------------------------------------------- | |
80 | +- | |
81 | + ref: tut-textarea | |
82 | + type: textarea | |
83 | + title: textarea | |
84 | + text: | | |
85 | + Resposta num bloco de texto que pode ser usado para introduzir código. | |
86 | + A resposta é avaliada por um programa externo. | |
87 | + O programa externo, recebe a resposta no stdin e devolve a classificação no stdout. | |
88 | + Neste exemplo, o programa de avaliação verifica se a resposta contém as três palavras red, green e blue. | |
89 | + correct: correct/correct-question.py | |
90 | + # opcionais e defaults | |
91 | + lines: 3 | |
92 | + timeout: 5 | ... | ... |
... | ... | @@ -0,0 +1,56 @@ |
1 | +#============================================================================= | |
2 | +# The test reference should be a unique identifier. It is saved in the database | |
3 | +# so that queries for the results can be done in the terminal with | |
4 | +# $ sqlite3 students.db "select * from tests where ref='demo'" | |
5 | +ref: tutorial | |
6 | + | |
7 | +# (optional, default: '') You may wish to refer the course, year or kind of test | |
8 | +title: Teste tutorial | |
9 | + | |
10 | +# (optional) duration in minutes FIXME | |
11 | +duration: 90 | |
12 | + | |
13 | +# Database with student credentials and grades of all questions and tests done | |
14 | +# The database is an sqlite3 file generate with the script initdb.py | |
15 | +database: demo/students.db | |
16 | + | |
17 | +# Generate a file for each test done by a student. | |
18 | +# It includes the questions, answers and grades. | |
19 | +answers_dir: demo/ans | |
20 | + | |
21 | +# (optional, default: False) Show points for each question, scale 0-20. | |
22 | +show_points: True | |
23 | + | |
24 | +# (optional, default: False) Show hints if available | |
25 | +show_hints: True | |
26 | + | |
27 | +# (optional, default: False) Show lots of information for debugging | |
28 | +# debug: True | |
29 | + | |
30 | +#----------------------------------------------------------------------------- | |
31 | +# Base path applied to the questions files and all the scripts | |
32 | +# including question generators and correctors. | |
33 | +# Either absolute path or relative to current directory can be used. | |
34 | +questions_dir: demo/questions | |
35 | + | |
36 | +# (optional) List of files containing questions in yaml format. | |
37 | +# Selected questions will be obtained from these files. | |
38 | +# If undefined, all yaml files in questions_dir are loaded (not recommended). | |
39 | +files: | |
40 | + - questions-tutorial.yaml | |
41 | + | |
42 | +# This is the list of questions that will make up the test. | |
43 | +# The order is preserved. | |
44 | +# There are several ways to define each question (explained below). | |
45 | +questions: | |
46 | + - tut-information | |
47 | + - tut-success | |
48 | + - tut-warning | |
49 | + - tut-alert | |
50 | + | |
51 | + - tut-radio | |
52 | + - tut-checkbox | |
53 | + - tut-text | |
54 | + - tut-text-regex | |
55 | + - tut-numeric-interval | |
56 | + - tut-textarea | ... | ... |
questions.py
... | ... | @@ -275,7 +275,7 @@ class QuestionTextRegex(Question): |
275 | 275 | |
276 | 276 | |
277 | 277 | # =========================================================================== |
278 | -class QuestionTextNumeric(Question): | |
278 | +class QuestionNumericInterval(Question): | |
279 | 279 | '''An instance of QuestionTextNumeric will always have the keys: |
280 | 280 | type (str) |
281 | 281 | text (str) |
... | ... | @@ -302,6 +302,7 @@ class QuestionTextNumeric(Question): |
302 | 302 | try: |
303 | 303 | answer = float(self['answer']) |
304 | 304 | |
305 | + # TODO: | |
305 | 306 | # alternative using locale (1.2 vs 1,2) |
306 | 307 | # import locale |
307 | 308 | # locale.setlocale(locale.LC_ALL, 'pt_PT') |
... | ... | @@ -401,8 +402,8 @@ class QuestionFactory(dict): |
401 | 402 | 'radio' : QuestionRadio, |
402 | 403 | 'checkbox' : QuestionCheckbox, |
403 | 404 | 'text' : QuestionText, |
404 | - 'text_regex': QuestionTextRegex, 'text-regex': QuestionTextRegex, | |
405 | - 'text_numeric': QuestionTextNumeric, 'text-numeric': QuestionTextNumeric, | |
405 | + 'text-regex': QuestionTextRegex, | |
406 | + 'numeric-interval': QuestionNumericInterval, | |
406 | 407 | 'textarea' : QuestionTextArea, |
407 | 408 | # -- informative panels -- |
408 | 409 | 'information': QuestionInformation, 'info': QuestionInformation, |
... | ... | @@ -420,22 +421,17 @@ class QuestionFactory(dict): |
420 | 421 | # Add single question provided in a dictionary. |
421 | 422 | # After this, each question will have at least 'ref' and 'type' keys. |
422 | 423 | # ----------------------------------------------------------------------- |
423 | - def add(self, question): | |
424 | - # if ref missing try ref='/path/file.yaml:3' | |
425 | - try: | |
426 | - question.setdefault('ref', question['filename'] + ':' + str(question['index'])) | |
427 | - except KeyError: | |
428 | - logger.error('Missing "ref". Cannot add question to the pool.') | |
429 | - return | |
424 | + def add_question(self, question): | |
425 | + # if missing defaults to ref='/path/file.yaml:3' | |
426 | + question.setdefault('ref', f'{question["filename"]}:{question["index"]}') | |
430 | 427 | |
431 | - # check duplicate references | |
432 | 428 | if question['ref'] in self: |
433 | - logger.error(f'Duplicate reference "{question["ref"]}". Replacing the original one!') | |
429 | + logger.error(f'Duplicate reference "{question["ref"]}" replaces the original.') | |
434 | 430 | |
435 | 431 | question.setdefault('type', 'information') |
436 | 432 | |
437 | 433 | self[question['ref']] = question |
438 | - logger.debug('Added question "{0}" to the pool.'.format(question['ref'])) | |
434 | + logger.debug(f'Added question "{question["ref"]}" to the pool.') | |
439 | 435 | |
440 | 436 | # ----------------------------------------------------------------------- |
441 | 437 | # load single YAML questions file |
... | ... | @@ -453,20 +449,19 @@ class QuestionFactory(dict): |
453 | 449 | |
454 | 450 | questions = load_yaml(fullpath, default=[]) |
455 | 451 | |
456 | - n = 0 | |
457 | 452 | for i, q in enumerate(questions): |
458 | - if isinstance(q, dict): | |
453 | + try: | |
459 | 454 | q.update({ |
460 | 455 | 'filename': filename, |
461 | 456 | 'path': dirname, |
462 | - 'index': i # position in the file, 0 based | |
457 | + 'index': i # position in the file, 0 based | |
463 | 458 | }) |
464 | - self.add(q) # add question | |
465 | - n += 1 # counter | |
466 | - else: | |
467 | - logger.error(f'Question index {i} from file {pathfile} is not a dictionary. Skipped!') | |
459 | + except AttributeError: | |
460 | + logger.error(f'Question {pathfile}:{i} is not a dictionary. Skipped!') | |
461 | + else: | |
462 | + self.add_question(q) | |
468 | 463 | |
469 | - logger.info(f'Loaded {n} questions from "{pathfile}".') | |
464 | + logger.info(f'Loaded {len(self)} questions from "{pathfile}".') | |
470 | 465 | |
471 | 466 | # ----------------------------------------------------------------------- |
472 | 467 | # load multiple YAML question files | ... | ... |
serve.py
1 | -#!/usr/bin/env python3 | |
1 | +#!/usr/bin/env python3.6 | |
2 | 2 | |
3 | 3 | # base |
4 | 4 | from os import path |
... | ... | @@ -26,7 +26,7 @@ class WebApplication(tornado.web.Application): |
26 | 26 | (r'/login', LoginHandler), |
27 | 27 | (r'/logout', LogoutHandler), |
28 | 28 | (r'/test', TestHandler), |
29 | - (r'/review', ReviewHandler), # FIXME | |
29 | + (r'/review', ReviewHandler), | |
30 | 30 | (r'/admin', AdminHandler), |
31 | 31 | # (r'/change_password', ChangePasswordHandler), |
32 | 32 | (r'/static/(.+)', FileHandler), # FIXME |
... | ... | @@ -76,7 +76,7 @@ class LoginHandler(BaseHandler): |
76 | 76 | self.set_secure_cookie("user", str(uid), expires_days=30) |
77 | 77 | self.redirect(self.get_argument("next", "/")) |
78 | 78 | else: |
79 | - self.render("login.html", error='Número ou senha incorrectos') | |
79 | + self.render("login.html", error='Não autorizado ou número/senha inválido') | |
80 | 80 | |
81 | 81 | # ------------------------------------------------------------------------- |
82 | 82 | class LogoutHandler(BaseHandler): |
... | ... | @@ -110,10 +110,8 @@ class TestHandler(BaseHandler): |
110 | 110 | 'radio': 'question-radio.html', |
111 | 111 | 'checkbox': 'question-checkbox.html', |
112 | 112 | 'text': 'question-text.html', |
113 | - # 'text_regex': 'question-text.html', | |
114 | 113 | 'text-regex': 'question-text.html', |
115 | - # 'text_numeric': 'question-text.html', | |
116 | - 'text-numeric': 'question-text.html', | |
114 | + 'numeric-interval': 'question-text.html', | |
117 | 115 | 'textarea': 'question-textarea.html', |
118 | 116 | # -- information panels -- |
119 | 117 | 'information': 'question-information.html', |
... | ... | @@ -153,7 +151,7 @@ class TestHandler(BaseHandler): |
153 | 151 | ans[i] = None |
154 | 152 | else: |
155 | 153 | ans[i] = ans[i][0] |
156 | - elif q['type'] in ('textarea', 'text', 'text-numeric', 'text-regex'): | |
154 | + elif q['type'] in ('text', 'text-regex', 'textarea', 'numeric-interval'): | |
157 | 155 | ans[i] = ans[i][0] |
158 | 156 | |
159 | 157 | self.testapp.correct_test(uid, ans) |
... | ... | @@ -170,10 +168,12 @@ class ReviewHandler(BaseHandler): |
170 | 168 | 'checkbox': 'review-question-checkbox.html', |
171 | 169 | 'text': 'review-question-text.html', |
172 | 170 | 'text-regex': 'review-question-text.html', |
173 | - 'text-numeric': 'review-question-text.html', | |
171 | + 'numeric-interval': 'review-question-text.html', | |
174 | 172 | 'textarea': 'review-question-text.html', |
175 | 173 | # -- information panels -- |
174 | + 'information': 'question-information.html', | |
176 | 175 | 'info': 'question-information.html', |
176 | + 'warning': 'question-warning.html', | |
177 | 177 | 'warn': 'question-warning.html', |
178 | 178 | 'alert': 'question-alert.html', |
179 | 179 | 'success': 'question-success.html', |
... | ... | @@ -322,6 +322,7 @@ def main(): |
322 | 322 | logging.critical('Failed to start application.') |
323 | 323 | sys.exit(1) |
324 | 324 | |
325 | + # --- start web application | |
325 | 326 | try: |
326 | 327 | webapp = WebApplication(app, debug=arg.debug) |
327 | 328 | except Exception as e: | ... | ... |
static/css/test.css
templates/question-success.html
templates/question-warning.html
templates/question.html
templates/review-question.html
... | ... | @@ -25,38 +25,39 @@ |
25 | 25 | {% if t['show_points'] %} |
26 | 26 | <p class="text-right"> |
27 | 27 | <small> |
28 | - (Cotação: {{ q['points'] }} pontos não normalizados) | |
28 | + (Cotação: {{ q['points'] }}) | |
29 | 29 | </small> |
30 | 30 | </p> |
31 | 31 | {% end %} |
32 | + </div> <!-- card-body --> | |
33 | + | |
34 | + {% if t['state'] == 'FINISHED' %} | |
35 | + <div class="card-footer"> | |
36 | + {% if q['grade'] > 0.99 %} | |
37 | + <p class="text-success"> | |
38 | + <i class="fa fa-thumbs-o-up" aria-hidden="true"></i> | |
39 | + {{ round(q['grade'] * q['points'], 2) }} | |
40 | + pontos<br> | |
41 | + {{ q['comments'] }} | |
42 | + </p> | |
43 | + {% elif q['grade'] > 0.49 %} | |
44 | + <p class="text-warning"> | |
45 | + <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> | |
46 | + {{ round(q['grade'] * q['points'], 2) }} | |
47 | + pontos<br> | |
48 | + {{ q['comments'] }} | |
49 | + </p> | |
50 | + {% else %} | |
51 | + <p class="text-danger"> | |
52 | + <i class="fa fa-thumbs-o-down" aria-hidden="true"></i> | |
53 | + {{ round(q['grade'] * q['points'], 2) }} | |
54 | + pontos<br> | |
55 | + {{ q['comments'] }} | |
56 | + </p> | |
57 | + {% end %} | |
58 | + </div> <!-- card-footer --> | |
59 | + {% end %} | |
32 | 60 | |
33 | - {% if t['state'] == 'FINISHED' %} | |
34 | - <div class="card-footer"> | |
35 | - {% if q['grade'] > 0.99 %} | |
36 | - <p class="text-success"> | |
37 | - <i class="fa fa-thumbs-o-up" aria-hidden="true"></i> | |
38 | - {{ round(q['grade'] * q['points'] / total_points * 20.0, 2)}} | |
39 | - pontos<br> | |
40 | - {{ q['comments'] }} | |
41 | - </p> | |
42 | - {% elif q['grade'] > 0.49 %} | |
43 | - <p class="text-warning"> | |
44 | - <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> | |
45 | - {{ round(q['grade'] * q['points'] / total_points * 20.0, 2) }} | |
46 | - pontos<br> | |
47 | - {{ q['comments'] }} | |
48 | - </p> | |
49 | - {% else %} | |
50 | - <p class="text-danger"> | |
51 | - <i class="fa fa-thumbs-o-down" aria-hidden="true"></i> | |
52 | - {{ round(q['grade'] * q['points'] / total_points * 20.0, 2) }} | |
53 | - pontos<br> | |
54 | - {{ q['comments'] }} | |
55 | - </p> | |
56 | - {% end %} | |
57 | - </div> | |
58 | - {% end %} | |
59 | - </div> | |
60 | 61 | |
61 | 62 | </div> |
62 | 63 | {% end %} |
63 | 64 | \ No newline at end of file | ... | ... |
templates/test.html
... | ... | @@ -28,9 +28,9 @@ |
28 | 28 | <!-- ===================================================================== --> |
29 | 29 | <body> |
30 | 30 | |
31 | -<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark"> | |
31 | +<nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-dark"> | |
32 | 32 | <a class="navbar-brand" href="#"> |
33 | - <i class="fa fa-clock-o" aria-hidden="true"></i> | |
33 | + <!-- <i class="fa fa-clock-o" aria-hidden="true"></i> --> | |
34 | 34 | <span id="clock"> --:-- </span> |
35 | 35 | </a> |
36 | 36 | <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> | ... | ... |
test.py
... | ... | @@ -129,9 +129,12 @@ class TestFactory(dict): |
129 | 129 | self.setdefault('title', '') |
130 | 130 | self.setdefault('show_hints', False) |
131 | 131 | self.setdefault('show_points', False) |
132 | + self.setdefault('scale_points', True) | |
133 | + self.setdefault('scale_max', 20.0) | |
134 | + self.setdefault('duration', 0) # FIXME unused | |
135 | + | |
132 | 136 | self.setdefault('debug', False) |
133 | 137 | self.setdefault('show_ref', False) |
134 | - self.setdefault('duration', 0) # FIXME unused | |
135 | 138 | |
136 | 139 | |
137 | 140 | # ----------------------------------------------------------------------- |
... | ... | @@ -140,6 +143,8 @@ class TestFactory(dict): |
140 | 143 | # ----------------------------------------------------------------------- |
141 | 144 | def generate(self, student): |
142 | 145 | test = [] |
146 | + total_points = 0.0 | |
147 | + | |
143 | 148 | for qq in self['questions']: |
144 | 149 | # generate Question() selected randomly from list of references |
145 | 150 | qref = random.choice(qq['ref']) |
... | ... | @@ -151,19 +156,26 @@ class TestFactory(dict): |
151 | 156 | continue |
152 | 157 | |
153 | 158 | # some defaults |
154 | - if q['type'] in ('information', 'warning', 'alert'): | |
159 | + if q['type'] in ('information', 'success', 'warning', 'alert'): | |
155 | 160 | q['points'] = qq.get('points', 0.0) |
156 | 161 | else: |
157 | 162 | q['points'] = qq.get('points', 1.0) |
158 | 163 | |
164 | + total_points += q['points'] | |
159 | 165 | test.append(q) |
160 | 166 | |
167 | + # normalize question points to scale | |
168 | + if self['scale_points']: | |
169 | + for q in test: | |
170 | + q['points'] *= self['scale_max'] / total_points | |
171 | + | |
161 | 172 | return Test({ |
162 | 173 | 'ref': self['ref'], |
163 | 174 | 'title': self['title'], # title of the test |
164 | 175 | 'student': student, # student id |
165 | 176 | 'questions': test, # list of questions |
166 | 177 | 'answers_dir': self['answers_dir'], |
178 | + # 'total_points': total_points, | |
167 | 179 | |
168 | 180 | # FIXME which ones are required? |
169 | 181 | 'show_hints': self['show_hints'], |
... | ... | @@ -218,17 +230,8 @@ class Test(dict): |
218 | 230 | self['finish_time'] = datetime.now() |
219 | 231 | self['state'] = 'FINISHED' |
220 | 232 | |
221 | - grade = 0.0 | |
222 | - total_points = 0.0 | |
223 | - for q in self['questions']: | |
224 | - grade += q.correct() * q['points'] | |
225 | - total_points += q['points'] | |
226 | - | |
227 | - if total_points > 0.0: | |
228 | - self['grade'] = round(20.0 * max(grade / total_points, 0.0), 1) | |
229 | - else: | |
230 | - logger.error(f'Student {self["student"]["number"]}: division by zero during correction. Total points must be positive.') | |
231 | - self['grade'] = 0.0 | |
233 | + grade = sum(q.correct()*q['points'] for q in self['questions']) | |
234 | + self['grade'] = round(grade, 1) | |
232 | 235 | |
233 | 236 | logger.info(f'Student {self["student"]["number"]}: correction gave {self["grade"]} points.') |
234 | 237 | return self['grade'] | ... | ... |