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 | # BUGS | 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 | - uniformizar question.py com a de aprendizations... | 10 | - uniformizar question.py com a de aprendizations... |
| 7 | - Review de um teste que foi apagado rebenta. | 11 | - Review de um teste que foi apagado rebenta. |
| 8 | - permitir eliminar teste a decorrer de modo a que o aluno possa recomeçar (e.g. noutro browser) | 12 | - permitir eliminar teste a decorrer de modo a que o aluno possa recomeçar (e.g. noutro browser) |
| @@ -11,13 +15,10 @@ | @@ -11,13 +15,10 @@ | ||
| 11 | # TODO | 15 | # TODO |
| 12 | 16 | ||
| 13 | - Gerar pdf's com todos os testes no final (pdfkit). | 17 | - Gerar pdf's com todos os testes no final (pdfkit). |
| 14 | -- testar SSL | ||
| 15 | - manter registo dos unfocus durante o teste e de qual a pergunta visivel nesse momento | 18 | - manter registo dos unfocus durante o teste e de qual a pergunta visivel nesse momento |
| 16 | 19 | ||
| 17 | - permitir varios testes, aluno escolhe qual o teste que quer fazer. | 20 | - permitir varios testes, aluno escolhe qual o teste que quer fazer. |
| 18 | -- usar thread.Lock para aceder a variaveis de estado? | ||
| 19 | - se ocorrer um erro na correcçao avisar aluno para contactar o professor. | 21 | - se ocorrer um erro na correcçao avisar aluno para contactar o professor. |
| 20 | -- implementar practice mode? | ||
| 21 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova? | 22 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova? |
| 22 | - 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 | - 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 | - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). | 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,6 +31,9 @@ | ||
| 30 | 31 | ||
| 31 | # FIXED | 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 | - qd user 0 faz logout rebenta. | 37 | - qd user 0 faz logout rebenta. |
| 34 | - 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. | 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 | - configuração do teste não joga bem com o do aprendizations. Em particular os scripts não ficam com o mesmo path!!! | 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 @@ | @@ -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 @@ | @@ -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,7 +275,7 @@ class QuestionTextRegex(Question): | ||
| 275 | 275 | ||
| 276 | 276 | ||
| 277 | # =========================================================================== | 277 | # =========================================================================== |
| 278 | -class QuestionTextNumeric(Question): | 278 | +class QuestionNumericInterval(Question): |
| 279 | '''An instance of QuestionTextNumeric will always have the keys: | 279 | '''An instance of QuestionTextNumeric will always have the keys: |
| 280 | type (str) | 280 | type (str) |
| 281 | text (str) | 281 | text (str) |
| @@ -302,6 +302,7 @@ class QuestionTextNumeric(Question): | @@ -302,6 +302,7 @@ class QuestionTextNumeric(Question): | ||
| 302 | try: | 302 | try: |
| 303 | answer = float(self['answer']) | 303 | answer = float(self['answer']) |
| 304 | 304 | ||
| 305 | + # TODO: | ||
| 305 | # alternative using locale (1.2 vs 1,2) | 306 | # alternative using locale (1.2 vs 1,2) |
| 306 | # import locale | 307 | # import locale |
| 307 | # locale.setlocale(locale.LC_ALL, 'pt_PT') | 308 | # locale.setlocale(locale.LC_ALL, 'pt_PT') |
| @@ -401,8 +402,8 @@ class QuestionFactory(dict): | @@ -401,8 +402,8 @@ class QuestionFactory(dict): | ||
| 401 | 'radio' : QuestionRadio, | 402 | 'radio' : QuestionRadio, |
| 402 | 'checkbox' : QuestionCheckbox, | 403 | 'checkbox' : QuestionCheckbox, |
| 403 | 'text' : QuestionText, | 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 | 'textarea' : QuestionTextArea, | 407 | 'textarea' : QuestionTextArea, |
| 407 | # -- informative panels -- | 408 | # -- informative panels -- |
| 408 | 'information': QuestionInformation, 'info': QuestionInformation, | 409 | 'information': QuestionInformation, 'info': QuestionInformation, |
| @@ -420,22 +421,17 @@ class QuestionFactory(dict): | @@ -420,22 +421,17 @@ class QuestionFactory(dict): | ||
| 420 | # Add single question provided in a dictionary. | 421 | # Add single question provided in a dictionary. |
| 421 | # After this, each question will have at least 'ref' and 'type' keys. | 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 | if question['ref'] in self: | 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 | question.setdefault('type', 'information') | 431 | question.setdefault('type', 'information') |
| 436 | 432 | ||
| 437 | self[question['ref']] = question | 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 | # load single YAML questions file | 437 | # load single YAML questions file |
| @@ -453,20 +449,19 @@ class QuestionFactory(dict): | @@ -453,20 +449,19 @@ class QuestionFactory(dict): | ||
| 453 | 449 | ||
| 454 | questions = load_yaml(fullpath, default=[]) | 450 | questions = load_yaml(fullpath, default=[]) |
| 455 | 451 | ||
| 456 | - n = 0 | ||
| 457 | for i, q in enumerate(questions): | 452 | for i, q in enumerate(questions): |
| 458 | - if isinstance(q, dict): | 453 | + try: |
| 459 | q.update({ | 454 | q.update({ |
| 460 | 'filename': filename, | 455 | 'filename': filename, |
| 461 | 'path': dirname, | 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 | # load multiple YAML question files | 467 | # load multiple YAML question files |
serve.py
| 1 | -#!/usr/bin/env python3 | 1 | +#!/usr/bin/env python3.6 |
| 2 | 2 | ||
| 3 | # base | 3 | # base |
| 4 | from os import path | 4 | from os import path |
| @@ -26,7 +26,7 @@ class WebApplication(tornado.web.Application): | @@ -26,7 +26,7 @@ class WebApplication(tornado.web.Application): | ||
| 26 | (r'/login', LoginHandler), | 26 | (r'/login', LoginHandler), |
| 27 | (r'/logout', LogoutHandler), | 27 | (r'/logout', LogoutHandler), |
| 28 | (r'/test', TestHandler), | 28 | (r'/test', TestHandler), |
| 29 | - (r'/review', ReviewHandler), # FIXME | 29 | + (r'/review', ReviewHandler), |
| 30 | (r'/admin', AdminHandler), | 30 | (r'/admin', AdminHandler), |
| 31 | # (r'/change_password', ChangePasswordHandler), | 31 | # (r'/change_password', ChangePasswordHandler), |
| 32 | (r'/static/(.+)', FileHandler), # FIXME | 32 | (r'/static/(.+)', FileHandler), # FIXME |
| @@ -76,7 +76,7 @@ class LoginHandler(BaseHandler): | @@ -76,7 +76,7 @@ class LoginHandler(BaseHandler): | ||
| 76 | self.set_secure_cookie("user", str(uid), expires_days=30) | 76 | self.set_secure_cookie("user", str(uid), expires_days=30) |
| 77 | self.redirect(self.get_argument("next", "/")) | 77 | self.redirect(self.get_argument("next", "/")) |
| 78 | else: | 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 | class LogoutHandler(BaseHandler): | 82 | class LogoutHandler(BaseHandler): |
| @@ -110,10 +110,8 @@ class TestHandler(BaseHandler): | @@ -110,10 +110,8 @@ class TestHandler(BaseHandler): | ||
| 110 | 'radio': 'question-radio.html', | 110 | 'radio': 'question-radio.html', |
| 111 | 'checkbox': 'question-checkbox.html', | 111 | 'checkbox': 'question-checkbox.html', |
| 112 | 'text': 'question-text.html', | 112 | 'text': 'question-text.html', |
| 113 | - # 'text_regex': 'question-text.html', | ||
| 114 | 'text-regex': 'question-text.html', | 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 | 'textarea': 'question-textarea.html', | 115 | 'textarea': 'question-textarea.html', |
| 118 | # -- information panels -- | 116 | # -- information panels -- |
| 119 | 'information': 'question-information.html', | 117 | 'information': 'question-information.html', |
| @@ -153,7 +151,7 @@ class TestHandler(BaseHandler): | @@ -153,7 +151,7 @@ class TestHandler(BaseHandler): | ||
| 153 | ans[i] = None | 151 | ans[i] = None |
| 154 | else: | 152 | else: |
| 155 | ans[i] = ans[i][0] | 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 | ans[i] = ans[i][0] | 155 | ans[i] = ans[i][0] |
| 158 | 156 | ||
| 159 | self.testapp.correct_test(uid, ans) | 157 | self.testapp.correct_test(uid, ans) |
| @@ -170,10 +168,12 @@ class ReviewHandler(BaseHandler): | @@ -170,10 +168,12 @@ class ReviewHandler(BaseHandler): | ||
| 170 | 'checkbox': 'review-question-checkbox.html', | 168 | 'checkbox': 'review-question-checkbox.html', |
| 171 | 'text': 'review-question-text.html', | 169 | 'text': 'review-question-text.html', |
| 172 | 'text-regex': 'review-question-text.html', | 170 | 'text-regex': 'review-question-text.html', |
| 173 | - 'text-numeric': 'review-question-text.html', | 171 | + 'numeric-interval': 'review-question-text.html', |
| 174 | 'textarea': 'review-question-text.html', | 172 | 'textarea': 'review-question-text.html', |
| 175 | # -- information panels -- | 173 | # -- information panels -- |
| 174 | + 'information': 'question-information.html', | ||
| 176 | 'info': 'question-information.html', | 175 | 'info': 'question-information.html', |
| 176 | + 'warning': 'question-warning.html', | ||
| 177 | 'warn': 'question-warning.html', | 177 | 'warn': 'question-warning.html', |
| 178 | 'alert': 'question-alert.html', | 178 | 'alert': 'question-alert.html', |
| 179 | 'success': 'question-success.html', | 179 | 'success': 'question-success.html', |
| @@ -322,6 +322,7 @@ def main(): | @@ -322,6 +322,7 @@ def main(): | ||
| 322 | logging.critical('Failed to start application.') | 322 | logging.critical('Failed to start application.') |
| 323 | sys.exit(1) | 323 | sys.exit(1) |
| 324 | 324 | ||
| 325 | + # --- start web application | ||
| 325 | try: | 326 | try: |
| 326 | webapp = WebApplication(app, debug=arg.debug) | 327 | webapp = WebApplication(app, debug=arg.debug) |
| 327 | except Exception as e: | 328 | except Exception as e: |
static/css/test.css
| 1 | /* Fixes navigation panel overlaying content */ | 1 | /* Fixes navigation panel overlaying content */ |
| 2 | +html { | ||
| 3 | + font-size: 14px; | ||
| 4 | +} | ||
| 5 | + | ||
| 2 | body { | 6 | body { |
| 3 | padding-top: 100px; /* make room at top of page for the navbar */ | 7 | padding-top: 100px; /* make room at top of page for the navbar */ |
| 4 | background: #aaa; | 8 | background: #aaa; |
templates/question-success.html
templates/question-warning.html
templates/question.html
templates/review-question.html
| @@ -25,38 +25,39 @@ | @@ -25,38 +25,39 @@ | ||
| 25 | {% if t['show_points'] %} | 25 | {% if t['show_points'] %} |
| 26 | <p class="text-right"> | 26 | <p class="text-right"> |
| 27 | <small> | 27 | <small> |
| 28 | - (Cotação: {{ q['points'] }} pontos não normalizados) | 28 | + (Cotação: {{ q['points'] }}) |
| 29 | </small> | 29 | </small> |
| 30 | </p> | 30 | </p> |
| 31 | {% end %} | 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 | </div> | 62 | </div> |
| 62 | {% end %} | 63 | {% end %} |
| 63 | \ No newline at end of file | 64 | \ No newline at end of file |
templates/test.html
| @@ -28,9 +28,9 @@ | @@ -28,9 +28,9 @@ | ||
| 28 | <!-- ===================================================================== --> | 28 | <!-- ===================================================================== --> |
| 29 | <body> | 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 | <a class="navbar-brand" href="#"> | 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 | <span id="clock"> --:-- </span> | 34 | <span id="clock"> --:-- </span> |
| 35 | </a> | 35 | </a> |
| 36 | <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> | 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,9 +129,12 @@ class TestFactory(dict): | ||
| 129 | self.setdefault('title', '') | 129 | self.setdefault('title', '') |
| 130 | self.setdefault('show_hints', False) | 130 | self.setdefault('show_hints', False) |
| 131 | self.setdefault('show_points', False) | 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 | self.setdefault('debug', False) | 136 | self.setdefault('debug', False) |
| 133 | self.setdefault('show_ref', False) | 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,6 +143,8 @@ class TestFactory(dict): | ||
| 140 | # ----------------------------------------------------------------------- | 143 | # ----------------------------------------------------------------------- |
| 141 | def generate(self, student): | 144 | def generate(self, student): |
| 142 | test = [] | 145 | test = [] |
| 146 | + total_points = 0.0 | ||
| 147 | + | ||
| 143 | for qq in self['questions']: | 148 | for qq in self['questions']: |
| 144 | # generate Question() selected randomly from list of references | 149 | # generate Question() selected randomly from list of references |
| 145 | qref = random.choice(qq['ref']) | 150 | qref = random.choice(qq['ref']) |
| @@ -151,19 +156,26 @@ class TestFactory(dict): | @@ -151,19 +156,26 @@ class TestFactory(dict): | ||
| 151 | continue | 156 | continue |
| 152 | 157 | ||
| 153 | # some defaults | 158 | # some defaults |
| 154 | - if q['type'] in ('information', 'warning', 'alert'): | 159 | + if q['type'] in ('information', 'success', 'warning', 'alert'): |
| 155 | q['points'] = qq.get('points', 0.0) | 160 | q['points'] = qq.get('points', 0.0) |
| 156 | else: | 161 | else: |
| 157 | q['points'] = qq.get('points', 1.0) | 162 | q['points'] = qq.get('points', 1.0) |
| 158 | 163 | ||
| 164 | + total_points += q['points'] | ||
| 159 | test.append(q) | 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 | return Test({ | 172 | return Test({ |
| 162 | 'ref': self['ref'], | 173 | 'ref': self['ref'], |
| 163 | 'title': self['title'], # title of the test | 174 | 'title': self['title'], # title of the test |
| 164 | 'student': student, # student id | 175 | 'student': student, # student id |
| 165 | 'questions': test, # list of questions | 176 | 'questions': test, # list of questions |
| 166 | 'answers_dir': self['answers_dir'], | 177 | 'answers_dir': self['answers_dir'], |
| 178 | + # 'total_points': total_points, | ||
| 167 | 179 | ||
| 168 | # FIXME which ones are required? | 180 | # FIXME which ones are required? |
| 169 | 'show_hints': self['show_hints'], | 181 | 'show_hints': self['show_hints'], |
| @@ -218,17 +230,8 @@ class Test(dict): | @@ -218,17 +230,8 @@ class Test(dict): | ||
| 218 | self['finish_time'] = datetime.now() | 230 | self['finish_time'] = datetime.now() |
| 219 | self['state'] = 'FINISHED' | 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 | logger.info(f'Student {self["student"]["number"]}: correction gave {self["grade"]} points.') | 236 | logger.info(f'Student {self["student"]["number"]}: correction gave {self["grade"]} points.') |
| 234 | return self['grade'] | 237 | return self['grade'] |