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