Commit 1d26fac2210c63fc7eb46239c9ddbc25969db2b7
1 parent
71a43de2
Exists in
master
and in
1 other branch
- updates BUGS.md and README.md.
- adds templates for loggers in config directory.
Showing
8 changed files
with
173 additions
and
77 deletions
Show diff stats
BUGS.md
| 1 | 1 | ||
| 2 | # BUGS | 2 | # BUGS |
| 3 | 3 | ||
| 4 | -- ensure pip nao funciona no ubuntu?... | ||
| 5 | -- acrescentar logger.conf que sirva de base. | ||
| 6 | -- exception sqlalchemy relacionada com threads. | ||
| 7 | - quando se clica no texto de uma opcao, salta para outro lado na pagina. | 4 | - quando se clica no texto de uma opcao, salta para outro lado na pagina. |
| 8 | - ordenacao das notas em /admin nao é numerica, é ascii... | 5 | - ordenacao das notas em /admin nao é numerica, é ascii... |
| 9 | - mensagems de erro do assembler aparecem na mesma linha na correcao e nao fazerm rendering do `$t`, ver se servidor faz parse do markdown dessas mensagens. | 6 | - mensagems de erro do assembler aparecem na mesma linha na correcao e nao fazerm rendering do `$t`, ver se servidor faz parse do markdown dessas mensagens. |
| @@ -12,21 +9,17 @@ | @@ -12,21 +9,17 @@ | ||
| 12 | - melhorar o botao de autorizar (desliga-se), usar antes um botao? | 9 | - melhorar o botao de autorizar (desliga-se), usar antes um botao? |
| 13 | e.g. retornar None quando nao ha alteracoes relativamente à última vez. | 10 | e.g. retornar None quando nao ha alteracoes relativamente à última vez. |
| 14 | ou usar push (websockets?) | 11 | ou usar push (websockets?) |
| 15 | -- pymips: nao pode executar syscalls do spim. | ||
| 16 | -- perguntas checkbox [right,wrong] com pelo menos uma opção correcta. | ||
| 17 | -- eventos unfocus? | 12 | +- lidar com eventos unfocus. |
| 18 | - servidor nao esta a lidar com eventos scroll/resize. ignorar? | 13 | - servidor nao esta a lidar com eventos scroll/resize. ignorar? |
| 19 | - Test.reset_answers() unused. | 14 | - Test.reset_answers() unused. |
| 20 | 15 | ||
| 21 | # TODO | 16 | # TODO |
| 22 | 17 | ||
| 18 | +- test: mostrar duração do teste com progressbar no navbar. | ||
| 23 | - submissao fazer um post ajax? | 19 | - submissao fazer um post ajax? |
| 24 | -- fazer package para instalar perguntations com pip. | ||
| 25 | - adicionar opcao para eliminar um teste em curso. | 20 | - adicionar opcao para eliminar um teste em curso. |
| 26 | -- gerar teste qd o prof autoriza? melhor nao, pode apagar o teste em curso. gerar previamente e manter uma pool de testes gerados? | ||
| 27 | - enviar resposta de cada pergunta individualmente. | 21 | - enviar resposta de cada pergunta individualmente. |
| 28 | - experimentar gerador de svg que inclua no markdown da pergunta e ver se funciona. | 22 | - experimentar gerador de svg que inclua no markdown da pergunta e ver se funciona. |
| 29 | -- suportar cotacao to teste diferente de 20 (e.g. para juntar perguntas em papel). opcao "points: 18" que normaliza total para 18 em vez de 20. | ||
| 30 | - quando ha varias perguntas para escolher, escolher sucessivamente em vez de aleatoriamente. | 23 | - quando ha varias perguntas para escolher, escolher sucessivamente em vez de aleatoriamente. |
| 31 | - como refrescar a tabela de admin sem fazer reload da pagina? | 24 | - como refrescar a tabela de admin sem fazer reload da pagina? |
| 32 | - botao "testar resposta" que valida codigo relativamente a syntax, mas nao classifica. perguntas devem ter opcao validate: script.py. Aluno pressiona botao e codigo é enviado para servidor para validação, feedback é mostrado na pagina de teste. | 25 | - botao "testar resposta" que valida codigo relativamente a syntax, mas nao classifica. perguntas devem ter opcao validate: script.py. Aluno pressiona botao e codigo é enviado para servidor para validação, feedback é mostrado na pagina de teste. |
| @@ -34,15 +27,12 @@ ou usar push (websockets?) | @@ -34,15 +27,12 @@ ou usar push (websockets?) | ||
| 34 | - test: Cada pergunta respondida é logo submetida. | 27 | - test: Cada pergunta respondida é logo submetida. |
| 35 | - test: calculadora javascript. | 28 | - test: calculadora javascript. |
| 36 | - admin: histograma das notas. | 29 | - admin: histograma das notas. |
| 37 | -- admin: gerar os testes no momento em que são autorizados, e não no login. <- se prof autoriza aluno que já esta a realizar teste pode fazer reset e destruir teste??? | ||
| 38 | - admin: mostrar as horas a que o teste terminou para os testes terminados. | 30 | - admin: mostrar as horas a que o teste terminou para os testes terminados. |
| 39 | - admin: histograma das notas. | 31 | - admin: histograma das notas. |
| 40 | - admin: mostrar teste gerado para aluno (tipo review). | 32 | - admin: mostrar teste gerado para aluno (tipo review). |
| 41 | -- test: mostrar duração do teste com progressbar no navbar. | ||
| 42 | -- fazer renderer para formulas com mathjax serverside (mathjax-node). | 33 | +- fazer renderer para formulas com mathjax serverside (mathjax-node) ou usar katex. |
| 43 | - fazer renderer para imagens, com links /file?ref=xpto;name=zzz.jpg | 34 | - fazer renderer para imagens, com links /file?ref=xpto;name=zzz.jpg |
| 44 | - fazer renderer para linguagem assembly mips? | 35 | - fazer renderer para linguagem assembly mips? |
| 45 | -- permitir eliminar teste a decorrer | ||
| 46 | - cancelar teste no menu admin. Dado o numero de aluno remove teste e faz logout do aluno. | 36 | - cancelar teste no menu admin. Dado o numero de aluno remove teste e faz logout do aluno. |
| 47 | - mathjax-node: | 37 | - mathjax-node: |
| 48 | sudo pkg install node npm | 38 | sudo pkg install node npm |
| @@ -57,16 +47,18 @@ ou usar push (websockets?) | @@ -57,16 +47,18 @@ ou usar push (websockets?) | ||
| 57 | - se ocorrer um erro na correcçao avisar aluno para contactar o professor. | 47 | - se ocorrer um erro na correcçao avisar aluno para contactar o professor. |
| 58 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova? | 48 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova? |
| 59 | - 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) | 49 | - 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) |
| 60 | -- single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). | ||
| 61 | - aviso na pagina principal para quem usa browser da treta | 50 | - aviso na pagina principal para quem usa browser da treta |
| 62 | - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput | 51 | - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput |
| 63 | - perguntas para professor corrigir mais tarde. | 52 | - perguntas para professor corrigir mais tarde. |
| 64 | - fazer uma calculadora javascript e por no menu. surge como modal | 53 | - fazer uma calculadora javascript e por no menu. surge como modal |
| 65 | -- GeoIP? | ||
| 66 | -- enviar logs para web? | ||
| 67 | 54 | ||
| 68 | # FIXED | 55 | # FIXED |
| 69 | 56 | ||
| 57 | +- suportar cotacao to teste diferente de 20 (e.g. para juntar perguntas em papel). opcao "points: 18" que normaliza total para 18 em vez de 20. | ||
| 58 | +- fazer package para instalar perguntations com pip. | ||
| 59 | +- pymips: nao pode executar syscalls do spim. | ||
| 60 | +- exception sqlalchemy relacionada com threads. | ||
| 61 | +- acrescentar logger.conf que sirva de base. | ||
| 70 | - questions.py textarea has a abspath which does not make sense! why is it there? not working for perguntations, but seems to work for aprendizations | 62 | - questions.py textarea has a abspath which does not make sense! why is it there? not working for perguntations, but seems to work for aprendizations |
| 71 | - textarea foi modificado em aprendizations para receber cmd line args. corrigir aqui tb. | 63 | - textarea foi modificado em aprendizations para receber cmd line args. corrigir aqui tb. |
| 72 | - usar npm para instalar javascript | 64 | - usar npm para instalar javascript |
README.md
| @@ -11,13 +11,13 @@ | @@ -11,13 +11,13 @@ | ||
| 11 | 11 | ||
| 12 | ## Requirements | 12 | ## Requirements |
| 13 | 13 | ||
| 14 | -The webserver is a python application that requires `>=python3.6` and `pip` to be | ||
| 15 | -installed. `npm` (Node package management) is also necessary to install the | ||
| 16 | -javascript libraries. | 14 | +The webserver is a python application that requires `>=python3.7` and `pip` to |
| 15 | +be installed. Node package management `npm` is also necessary in order to | ||
| 16 | +install the javascript libraries. | ||
| 17 | 17 | ||
| 18 | ```bash | 18 | ```bash |
| 19 | -sudo apt install python3 python3-pip python3-setuptools npm # Ubuntu | ||
| 20 | -sudo pkg install python36 py36-sqlite3 py36-pip py36-setuptools npm # FreeBSD | 19 | +sudo apt install python3 python3-pip npm # Ubuntu |
| 20 | +sudo pkg install python37 py37-sqlite3 py37-pip npm # FreeBSD | ||
| 21 | sudo port install python37 py37-pip py37-setuptools npm6 # MacOS | 21 | sudo port install python37 py37-pip py37-setuptools npm6 # MacOS |
| 22 | ``` | 22 | ``` |
| 23 | 23 | ||
| @@ -38,19 +38,15 @@ This file is usually in `~/.config/pip/` in Linux and FreeBSD. In MacOS it's in | @@ -38,19 +38,15 @@ This file is usually in `~/.config/pip/` in Linux and FreeBSD. In MacOS it's in | ||
| 38 | 38 | ||
| 39 | ## Installation | 39 | ## Installation |
| 40 | 40 | ||
| 41 | -Download and install (`USERNAME` is your account on bitbucket): | 41 | +Download and install: |
| 42 | 42 | ||
| 43 | ```bash | 43 | ```bash |
| 44 | -cd ~ | ||
| 45 | -git clone https://USERNAME@bitbucket.org/USERNAME/perguntations.git | 44 | +git clone https://git.xdi.uevora.pt/perguntations.git |
| 46 | cd perguntations | 45 | cd perguntations |
| 47 | npm install | 46 | npm install |
| 48 | pip3 install . | 47 | pip3 install . |
| 49 | ``` | 48 | ``` |
| 50 | 49 | ||
| 51 | -Here, the repository is installed in the user home directory. | ||
| 52 | -You may wish to adjust to somewhere else. | ||
| 53 | - | ||
| 54 | The command `npm` installs the javascript libraries and `pip3` installs the | 50 | The command `npm` installs the javascript libraries and `pip3` installs the |
| 55 | python webserver. | 51 | python webserver. |
| 56 | This will also install any required dependencies. | 52 | This will also install any required dependencies. |
| @@ -95,7 +91,7 @@ To run the demonstration test you need to initialize the database using one of | @@ -95,7 +91,7 @@ To run the demonstration test you need to initialize the database using one of | ||
| 95 | the following methods: | 91 | the following methods: |
| 96 | 92 | ||
| 97 | ```bash | 93 | ```bash |
| 98 | -cd ~/perguntations/demo | 94 | +cd perguntations/demo |
| 99 | 95 | ||
| 100 | initdb students.csv # initialize from a CSV file | 96 | initdb students.csv # initialize from a CSV file |
| 101 | initdb --admin # only adds the administrator account | 97 | initdb --admin # only adds the administrator account |
| @@ -107,20 +103,20 @@ database. | @@ -107,20 +103,20 @@ database. | ||
| 107 | The database stores user passwords and grades, but not the actual tests. | 103 | The database stores user passwords and grades, but not the actual tests. |
| 108 | 104 | ||
| 109 | A test is specified in a single `yaml` file. | 105 | A test is specified in a single `yaml` file. |
| 110 | -The demo already includes the `tutorial.yaml` that you can play with. | 106 | +The demo already includes the `demo.yaml` that you can play with. |
| 111 | 107 | ||
| 112 | The complete tests submitted by the students are stored in JSON files in the | 108 | The complete tests submitted by the students are stored in JSON files in the |
| 113 | -directory defined in `tutorial.yaml` under the option `answers_dir: ans`. | 109 | +directory defined in `demo.yaml` under the option `answers_dir: ans`. |
| 114 | We also have to create this directory manually: | 110 | We also have to create this directory manually: |
| 115 | 111 | ||
| 116 | ```bash | 112 | ```bash |
| 117 | mkdir ans # directory where the tests will be saved | 113 | mkdir ans # directory where the tests will be saved |
| 118 | ``` | 114 | ``` |
| 119 | 115 | ||
| 120 | -Start the server and run the `tutorial.yaml` test: | 116 | +Start the server and run the `demo.yaml` test: |
| 121 | 117 | ||
| 122 | ```bash | 118 | ```bash |
| 123 | -perguntations tutorial.yaml # run demo test | 119 | +perguntations demo.yaml # run demo test |
| 124 | ``` | 120 | ``` |
| 125 | 121 | ||
| 126 | Several options are available, run `perguntations --help` for a list. | 122 | Several options are available, run `perguntations --help` for a list. |
| @@ -205,7 +201,7 @@ sudo service pf start # enable pf firewall | @@ -205,7 +201,7 @@ sudo service pf start # enable pf firewall | ||
| 205 | ``` | 201 | ``` |
| 206 | 202 | ||
| 207 | Certificates are saved in `/usr/local/etc/letsencrypt/live/www.example.com/`. | 203 | Certificates are saved in `/usr/local/etc/letsencrypt/live/www.example.com/`. |
| 208 | -Copy them to the `certs` directory and change permissions: | 204 | +Copy them to the `~/.local/share/certs` directory and change permissions: |
| 209 | 205 | ||
| 210 | ```sh | 206 | ```sh |
| 211 | chmod 400 cert.pem privkey.pem | 207 | chmod 400 cert.pem privkey.pem |
| @@ -219,15 +215,14 @@ sudo certbot renew | @@ -219,15 +215,14 @@ sudo certbot renew | ||
| 219 | sudo service pf start # start firewall | 215 | sudo service pf start # start firewall |
| 220 | ``` | 216 | ``` |
| 221 | 217 | ||
| 222 | -Again, copy certificate files `privkey.pem` and `cert.pem` to the `certs` | ||
| 223 | -directory. | 218 | +Again, copy certificate files `privkey.pem` and `cert.pem` to `~/.local/share/certs`. |
| 224 | 219 | ||
| 225 | --- | 220 | --- |
| 226 | 221 | ||
| 227 | ## Troubleshooting | 222 | ## Troubleshooting |
| 228 | 223 | ||
| 229 | - The server tries to run `python3` so this command must be accessible from | 224 | - The server tries to run `python3` so this command must be accessible from |
| 230 | -user accounts. Currently, the minimum supported python version is 3.6. | 225 | +user accounts. Currently, the minimum supported python version is 3.7. |
| 231 | 226 | ||
| 232 | - If you are getting any `UnicodeEncodeError` type of errors that's because the | 227 | - If you are getting any `UnicodeEncodeError` type of errors that's because the |
| 233 | terminal is not supporting UTF-8. | 228 | terminal is not supporting UTF-8. |
| @@ -0,0 +1,59 @@ | @@ -0,0 +1,59 @@ | ||
| 1 | +--- | ||
| 2 | +version: 1 | ||
| 3 | + | ||
| 4 | +formatters: | ||
| 5 | + void: | ||
| 6 | + format: '' | ||
| 7 | + standard: | ||
| 8 | + format: '%(asctime)s | %(levelname)-8s | %(module)-16s:%(lineno)4d | %(thread)d | %(message)s' | ||
| 9 | + | ||
| 10 | + error: | ||
| 11 | + format: "\e[41m%(asctime)s | %(levelname)-8s | %(module)-16s:%(lineno)4d | %(thread)d | %(message)s\e[0m" | ||
| 12 | + | ||
| 13 | +handlers: | ||
| 14 | + default: | ||
| 15 | + level: 'DEBUG' | ||
| 16 | + class: 'logging.StreamHandler' | ||
| 17 | + formatter: 'standard' | ||
| 18 | + stream: 'ext://sys.stdout' | ||
| 19 | + | ||
| 20 | + error: | ||
| 21 | + level: 'ERROR' | ||
| 22 | + class: 'logging.StreamHandler' | ||
| 23 | + formatter: 'error' | ||
| 24 | + stream: 'ext://sys.stdout' | ||
| 25 | + | ||
| 26 | +loggers: | ||
| 27 | + '': | ||
| 28 | + handlers: ['default', 'error'] | ||
| 29 | + level: 'DEBUG' | ||
| 30 | + | ||
| 31 | + 'perguntations.app': | ||
| 32 | + handlers: ['default', 'error'] | ||
| 33 | + level: 'DEBUG' | ||
| 34 | + propagate: false | ||
| 35 | + | ||
| 36 | + 'perguntations.models': | ||
| 37 | + handlers: ['default', 'error'] | ||
| 38 | + level: 'DEBUG' | ||
| 39 | + propagate: false | ||
| 40 | + | ||
| 41 | + 'perguntations.questions': | ||
| 42 | + handlers: ['default', 'error'] | ||
| 43 | + level: 'DEBUG' | ||
| 44 | + propagate: false | ||
| 45 | + | ||
| 46 | + 'perguntations.test': | ||
| 47 | + handlers: ['default', 'error'] | ||
| 48 | + level: 'DEBUG' | ||
| 49 | + propagate: false | ||
| 50 | + | ||
| 51 | + 'perguntations.tools': | ||
| 52 | + handlers: ['default', 'error'] | ||
| 53 | + level: 'DEBUG' | ||
| 54 | + propagate: false | ||
| 55 | + | ||
| 56 | + 'perguntations.parser_markdown': | ||
| 57 | + handlers: ['default', 'error'] | ||
| 58 | + level: 'DEBUG' | ||
| 59 | + propagate: false |
| @@ -0,0 +1,51 @@ | @@ -0,0 +1,51 @@ | ||
| 1 | +--- | ||
| 2 | +version: 1 | ||
| 3 | + | ||
| 4 | +formatters: | ||
| 5 | + void: | ||
| 6 | + format: '' | ||
| 7 | + standard: | ||
| 8 | + format: '%(asctime)s | %(levelname)-8s | %(message)s' | ||
| 9 | + datefmt: '%Y-%m-%d %H:%M:%S' | ||
| 10 | + | ||
| 11 | +handlers: | ||
| 12 | + default: | ||
| 13 | + level: 'INFO' | ||
| 14 | + class: 'logging.StreamHandler' | ||
| 15 | + formatter: 'standard' | ||
| 16 | + stream: 'ext://sys.stdout' | ||
| 17 | + | ||
| 18 | +loggers: | ||
| 19 | + '': | ||
| 20 | + handlers: ['default'] | ||
| 21 | + level: 'INFO' | ||
| 22 | + | ||
| 23 | + 'perguntations.app': | ||
| 24 | + handlers: ['default'] | ||
| 25 | + level: 'INFO' | ||
| 26 | + propagate: false | ||
| 27 | + | ||
| 28 | + 'perguntations.models': | ||
| 29 | + handlers: ['default'] | ||
| 30 | + level: 'INFO' | ||
| 31 | + propagate: false | ||
| 32 | + | ||
| 33 | + 'perguntations.questions': | ||
| 34 | + handlers: ['default'] | ||
| 35 | + level: 'INFO' | ||
| 36 | + propagate: false | ||
| 37 | + | ||
| 38 | + 'perguntations.test': | ||
| 39 | + handlers: ['default'] | ||
| 40 | + level: 'INFO' | ||
| 41 | + propagate: false | ||
| 42 | + | ||
| 43 | + 'perguntations.tools': | ||
| 44 | + handlers: ['default'] | ||
| 45 | + level: 'INFO' | ||
| 46 | + propagate: false | ||
| 47 | + | ||
| 48 | + 'perguntations.parser_markdown': | ||
| 49 | + handlers: ['default'] | ||
| 50 | + level: 'INFO' | ||
| 51 | + propagate: false |
perguntations/app.py
| @@ -45,7 +45,7 @@ async def hash_password(pw): | @@ -45,7 +45,7 @@ async def hash_password(pw): | ||
| 45 | # Application | 45 | # Application |
| 46 | # ============================================================================ | 46 | # ============================================================================ |
| 47 | class App(object): | 47 | class App(object): |
| 48 | - # ----------------------------------------------------------------------- | 48 | + # ------------------------------------------------------------------------ |
| 49 | # helper to manage db sessions using the `with` statement, for example | 49 | # helper to manage db sessions using the `with` statement, for example |
| 50 | # with self.db_session() as s: s.query(...) | 50 | # with self.db_session() as s: s.query(...) |
| 51 | @contextmanager | 51 | @contextmanager |
| @@ -98,14 +98,14 @@ class App(object): | @@ -98,14 +98,14 @@ class App(object): | ||
| 98 | for student in self.get_all_students(): | 98 | for student in self.get_all_students(): |
| 99 | self.allow_student(student[0]) | 99 | self.allow_student(student[0]) |
| 100 | 100 | ||
| 101 | - # ----------------------------------------------------------------------- | 101 | + # ------------------------------------------------------------------------ |
| 102 | def exit(self): | 102 | def exit(self): |
| 103 | if len(self.online) > 1: | 103 | if len(self.online) > 1: |
| 104 | online_students = ', '.join(self.online) | 104 | online_students = ', '.join(self.online) |
| 105 | logger.warning(f'Students still online: {online_students}') | 105 | logger.warning(f'Students still online: {online_students}') |
| 106 | logger.critical('----------- !!! Server terminated !!! -----------') | 106 | logger.critical('----------- !!! Server terminated !!! -----------') |
| 107 | 107 | ||
| 108 | - # ----------------------------------------------------------------------- | 108 | + # ------------------------------------------------------------------------ |
| 109 | async def login(self, uid, try_pw): | 109 | async def login(self, uid, try_pw): |
| 110 | if uid not in self.allowed and uid != '0': # not allowed | 110 | if uid not in self.allowed and uid != '0': # not allowed |
| 111 | logger.warning(f'Student {uid}: not allowed to login.') | 111 | logger.warning(f'Student {uid}: not allowed to login.') |
| @@ -136,12 +136,12 @@ class App(object): | @@ -136,12 +136,12 @@ class App(object): | ||
| 136 | logger.info(f'Student {uid}: wrong password.') | 136 | logger.info(f'Student {uid}: wrong password.') |
| 137 | return False | 137 | return False |
| 138 | 138 | ||
| 139 | - # ----------------------------------------------------------------------- | 139 | + # ------------------------------------------------------------------------ |
| 140 | def logout(self, uid): | 140 | def logout(self, uid): |
| 141 | self.online.pop(uid, None) # remove from dict if exists | 141 | self.online.pop(uid, None) # remove from dict if exists |
| 142 | logger.info(f'Student {uid}: logged out.') | 142 | logger.info(f'Student {uid}: logged out.') |
| 143 | 143 | ||
| 144 | - # ----------------------------------------------------------------------- | 144 | + # ------------------------------------------------------------------------ |
| 145 | async def generate_test(self, uid): | 145 | async def generate_test(self, uid): |
| 146 | if uid in self.online: | 146 | if uid in self.online: |
| 147 | logger.info(f'Student {uid}: generating new test.') | 147 | logger.info(f'Student {uid}: generating new test.') |
| @@ -154,7 +154,7 @@ class App(object): | @@ -154,7 +154,7 @@ class App(object): | ||
| 154 | # this implies an error in the code. should never be here! | 154 | # this implies an error in the code. should never be here! |
| 155 | logger.critical(f'Student {uid}: offline, can\'t generate test') | 155 | logger.critical(f'Student {uid}: offline, can\'t generate test') |
| 156 | 156 | ||
| 157 | - # ----------------------------------------------------------------------- | 157 | + # ------------------------------------------------------------------------ |
| 158 | # ans is a dictionary {question_index: answer, ...} | 158 | # ans is a dictionary {question_index: answer, ...} |
| 159 | # for example: {0:'hello', 1:[1,2]} | 159 | # for example: {0:'hello', 1:[1,2]} |
| 160 | async def correct_test(self, uid, ans): | 160 | async def correct_test(self, uid, ans): |
| @@ -199,7 +199,7 @@ class App(object): | @@ -199,7 +199,7 @@ class App(object): | ||
| 199 | logger.info(f'Student {uid}: database updated.') | 199 | logger.info(f'Student {uid}: database updated.') |
| 200 | return grade | 200 | return grade |
| 201 | 201 | ||
| 202 | - # ----------------------------------------------------------------------- | 202 | + # ------------------------------------------------------------------------ |
| 203 | def giveup_test(self, uid): | 203 | def giveup_test(self, uid): |
| 204 | t = self.online[uid]['test'] | 204 | t = self.online[uid]['test'] |
| 205 | t.giveup() | 205 | t.giveup() |
| @@ -226,7 +226,7 @@ class App(object): | @@ -226,7 +226,7 @@ class App(object): | ||
| 226 | logger.info(f'Student {uid}: gave up.') | 226 | logger.info(f'Student {uid}: gave up.') |
| 227 | return t | 227 | return t |
| 228 | 228 | ||
| 229 | - # ----------------------------------------------------------------------- | 229 | + # ------------------------------------------------------------------------ |
| 230 | 230 | ||
| 231 | # --- helpers (getters) | 231 | # --- helpers (getters) |
| 232 | # def get_student_name(self, uid): | 232 | # def get_student_name(self, uid): |
perguntations/serve.py
| @@ -117,7 +117,6 @@ class LogoutHandler(BaseHandler): | @@ -117,7 +117,6 @@ class LogoutHandler(BaseHandler): | ||
| 117 | # ---------------------------------------------------------------------------- | 117 | # ---------------------------------------------------------------------------- |
| 118 | # handles root / to redirect students to /test and admininistrator to /admin | 118 | # handles root / to redirect students to /test and admininistrator to /admin |
| 119 | # ---------------------------------------------------------------------------- | 119 | # ---------------------------------------------------------------------------- |
| 120 | -# TODO list available tests | ||
| 121 | class RootHandler(BaseHandler): | 120 | class RootHandler(BaseHandler): |
| 122 | @tornado.web.authenticated | 121 | @tornado.web.authenticated |
| 123 | def get(self): | 122 | def get(self): |
| @@ -168,9 +167,9 @@ class FileHandler(BaseHandler): | @@ -168,9 +167,9 @@ class FileHandler(BaseHandler): | ||
| 168 | break # for loop | 167 | break # for loop |
| 169 | 168 | ||
| 170 | 169 | ||
| 171 | -# ------------------------------------------------------------------------- | 170 | +# ---------------------------------------------------------------------------- |
| 172 | # Test shown to students | 171 | # Test shown to students |
| 173 | -# ------------------------------------------------------------------------- | 172 | +# ---------------------------------------------------------------------------- |
| 174 | class TestHandler(BaseHandler): | 173 | class TestHandler(BaseHandler): |
| 175 | _templates = { | 174 | _templates = { |
| 176 | 'radio': 'question-radio.html', | 175 | 'radio': 'question-radio.html', |
| @@ -190,7 +189,7 @@ class TestHandler(BaseHandler): | @@ -190,7 +189,7 @@ class TestHandler(BaseHandler): | ||
| 190 | @tornado.web.authenticated | 189 | @tornado.web.authenticated |
| 191 | async def get(self): | 190 | async def get(self): |
| 192 | uid = self.current_user | 191 | uid = self.current_user |
| 193 | - t = self.testapp.get_student_test(uid) # reload page returns same test | 192 | + t = self.testapp.get_student_test(uid) # reloading returns same test |
| 194 | if t is None: | 193 | if t is None: |
| 195 | t = await self.testapp.generate_test(uid) | 194 | t = await self.testapp.generate_test(uid) |
| 196 | self.render('test.html', t=t, md=md_to_html, templ=self._templates) | 195 | self.render('test.html', t=t, md=md_to_html, templ=self._templates) |
| @@ -206,11 +205,11 @@ class TestHandler(BaseHandler): | @@ -206,11 +205,11 @@ class TestHandler(BaseHandler): | ||
| 206 | t = self.testapp.get_student_test(uid) | 205 | t = self.testapp.get_student_test(uid) |
| 207 | ans = {} | 206 | ans = {} |
| 208 | for i, q in enumerate(t['questions']): | 207 | for i, q in enumerate(t['questions']): |
| 209 | - qid = str(i) # question id | 208 | + qid = str(i) |
| 210 | if 'answered-' + qid in self.request.arguments: | 209 | if 'answered-' + qid in self.request.arguments: |
| 211 | ans[i] = self.get_body_arguments(qid) | 210 | ans[i] = self.get_body_arguments(qid) |
| 212 | 211 | ||
| 213 | - # remove list when it does not make sense... | 212 | + # remove enclosing list in some question types |
| 214 | if q['type'] == 'radio': | 213 | if q['type'] == 'radio': |
| 215 | if not ans[i]: | 214 | if not ans[i]: |
| 216 | ans[i] = None | 215 | ans[i] = None |
| @@ -220,17 +219,18 @@ class TestHandler(BaseHandler): | @@ -220,17 +219,18 @@ class TestHandler(BaseHandler): | ||
| 220 | 'numeric-interval'): | 219 | 'numeric-interval'): |
| 221 | ans[i] = ans[i][0] | 220 | ans[i] = ans[i][0] |
| 222 | 221 | ||
| 222 | + # correct answered questions and logout | ||
| 223 | await self.testapp.correct_test(uid, ans) | 223 | await self.testapp.correct_test(uid, ans) |
| 224 | - | ||
| 225 | self.testapp.logout(uid) | 224 | self.testapp.logout(uid) |
| 226 | self.clear_cookie('user') | 225 | self.clear_cookie('user') |
| 227 | 226 | ||
| 227 | + # show final grade and grades of other tests in the database | ||
| 228 | allgrades = self.testapp.get_student_grades_from_all_tests(uid) | 228 | allgrades = self.testapp.get_student_grades_from_all_tests(uid) |
| 229 | self.render('grade.html', t=t, allgrades=allgrades) | 229 | self.render('grade.html', t=t, allgrades=allgrades) |
| 230 | 230 | ||
| 231 | 231 | ||
| 232 | -# ------------------------------------------------------------------------- | ||
| 233 | -# FIXME this should be a post in the test with command giveup instead of correct... | 232 | +# ---------------------------------------------------------------------------- |
| 233 | +# FIXME should be a post in the test with command giveup instead of correct... | ||
| 234 | # class GiveupHandler(BaseHandler): | 234 | # class GiveupHandler(BaseHandler): |
| 235 | # @tornado.web.authenticated | 235 | # @tornado.web.authenticated |
| 236 | # def get(self): | 236 | # def get(self): |
| @@ -242,7 +242,7 @@ class TestHandler(BaseHandler): | @@ -242,7 +242,7 @@ class TestHandler(BaseHandler): | ||
| 242 | # self.render('grade.html', t=t, allgrades=self.testapp.get_student_grades_from_all_tests(uid)) | 242 | # self.render('grade.html', t=t, allgrades=self.testapp.get_student_grades_from_all_tests(uid)) |
| 243 | 243 | ||
| 244 | 244 | ||
| 245 | -# --- REVIEW ------------------------------------------------------------- | 245 | +# --- REVIEW ----------------------------------------------------------------- |
| 246 | class ReviewHandler(BaseHandler): | 246 | class ReviewHandler(BaseHandler): |
| 247 | SUPPORTED_METHODS = ['GET'] | 247 | SUPPORTED_METHODS = ['GET'] |
| 248 | 248 | ||
| @@ -330,7 +330,7 @@ class AdminHandler(BaseHandler): | @@ -330,7 +330,7 @@ class AdminHandler(BaseHandler): | ||
| 330 | logging.error(f'Unknown command: "{cmd}"') | 330 | logging.error(f'Unknown command: "{cmd}"') |
| 331 | 331 | ||
| 332 | 332 | ||
| 333 | -# ------------------------------------------------------------------------- | 333 | +# ---------------------------------------------------------------------------- |
| 334 | def signal_handler(signal, frame): | 334 | def signal_handler(signal, frame): |
| 335 | r = input(' --> Stop webserver? (yes/no) ') | 335 | r = input(' --> Stop webserver? (yes/no) ') |
| 336 | if r in ('yes', 'YES'): | 336 | if r in ('yes', 'YES'): |
| @@ -339,7 +339,7 @@ def signal_handler(signal, frame): | @@ -339,7 +339,7 @@ def signal_handler(signal, frame): | ||
| 339 | sys.exit(0) | 339 | sys.exit(0) |
| 340 | 340 | ||
| 341 | 341 | ||
| 342 | -# ------------------------------------------------------------------------- | 342 | +# ---------------------------------------------------------------------------- |
| 343 | def parse_cmdline_arguments(): | 343 | def parse_cmdline_arguments(): |
| 344 | parser = argparse.ArgumentParser( | 344 | parser = argparse.ArgumentParser( |
| 345 | description='Server for online tests. Enrolled students and tests ' | 345 | description='Server for online tests. Enrolled students and tests ' |
| @@ -363,7 +363,7 @@ def parse_cmdline_arguments(): | @@ -363,7 +363,7 @@ def parse_cmdline_arguments(): | ||
| 363 | return parser.parse_args() | 363 | return parser.parse_args() |
| 364 | 364 | ||
| 365 | 365 | ||
| 366 | -# ------------------------------------------------------------------------- | 366 | +# ---------------------------------------------------------------------------- |
| 367 | def get_logger_config(debug=False): | 367 | def get_logger_config(debug=False): |
| 368 | if debug: | 368 | if debug: |
| 369 | filename = 'logger-debug.yaml' | 369 | filename = 'logger-debug.yaml' |
| @@ -409,9 +409,9 @@ def get_logger_config(debug=False): | @@ -409,9 +409,9 @@ def get_logger_config(debug=False): | ||
| 409 | return load_yaml(config_file, default=default_config) | 409 | return load_yaml(config_file, default=default_config) |
| 410 | 410 | ||
| 411 | 411 | ||
| 412 | -# ------------------------------------------------------------------------- | 412 | +# ---------------------------------------------------------------------------- |
| 413 | # Tornado web server | 413 | # Tornado web server |
| 414 | -# ------------------------------------------------------------------------- | 414 | +# ---------------------------------------------------------------------------- |
| 415 | def main(): | 415 | def main(): |
| 416 | args = parse_cmdline_arguments() | 416 | args = parse_cmdline_arguments() |
| 417 | 417 | ||
| @@ -477,6 +477,6 @@ def main(): | @@ -477,6 +477,6 @@ def main(): | ||
| 477 | raise | 477 | raise |
| 478 | 478 | ||
| 479 | 479 | ||
| 480 | -# ------------------------------------------------------------------------- | 480 | +# ---------------------------------------------------------------------------- |
| 481 | if __name__ == "__main__": | 481 | if __name__ == "__main__": |
| 482 | main() | 482 | main() |
perguntations/templates/test.html
| @@ -120,7 +120,8 @@ | @@ -120,7 +120,8 @@ | ||
| 120 | </div> | 120 | </div> |
| 121 | <div class="modal-body"> | 121 | <div class="modal-body"> |
| 122 | O teste será enviado para classificação e já não poderá voltar atrás. | 122 | O teste será enviado para classificação e já não poderá voltar atrás. |
| 123 | - Antes de submeter, veja se respondeu a todas as questões e desactive as que não pretende classificar. | 123 | + Antes de submeter, verifique se respondeu a todas as questões. |
| 124 | + Desactive as perguntas que não pretende classificar para evitar eventuais penalizações. | ||
| 124 | </div> | 125 | </div> |
| 125 | <div class="modal-footer"> | 126 | <div class="modal-footer"> |
| 126 | <button type="button" class="btn btn-danger btn-lg" data-dismiss="modal">Oops, NÃO!!!</button> | 127 | <button type="button" class="btn btn-danger btn-lg" data-dismiss="modal">Oops, NÃO!!!</button> |
perguntations/test.py
| @@ -14,22 +14,22 @@ from perguntations.tools import load_yaml | @@ -14,22 +14,22 @@ from perguntations.tools import load_yaml | ||
| 14 | logger = logging.getLogger(__name__) | 14 | logger = logging.getLogger(__name__) |
| 15 | 15 | ||
| 16 | 16 | ||
| 17 | -# =========================================================================== | 17 | +# ============================================================================ |
| 18 | class TestFactoryException(Exception): | 18 | class TestFactoryException(Exception): |
| 19 | pass | 19 | pass |
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | -# =========================================================================== | 22 | +# ============================================================================ |
| 23 | # Each instance of TestFactory() is a test generator. | 23 | # Each instance of TestFactory() is a test generator. |
| 24 | # For example, if we want to serve two different tests, then we need two | 24 | # For example, if we want to serve two different tests, then we need two |
| 25 | # instances of TestFactory(), one for each test. | 25 | # instances of TestFactory(), one for each test. |
| 26 | -# =========================================================================== | 26 | +# ============================================================================ |
| 27 | class TestFactory(dict): | 27 | class TestFactory(dict): |
| 28 | - # ----------------------------------------------------------------------- | 28 | + # ------------------------------------------------------------------------ |
| 29 | # Loads configuration from yaml file, then overrides some configurations | 29 | # Loads configuration from yaml file, then overrides some configurations |
| 30 | # using the conf argument. | 30 | # using the conf argument. |
| 31 | # Base questions are added to a pool of questions factories. | 31 | # Base questions are added to a pool of questions factories. |
| 32 | - # ----------------------------------------------------------------------- | 32 | + # ------------------------------------------------------------------------ |
| 33 | def __init__(self, conf): | 33 | def __init__(self, conf): |
| 34 | # --- set test configutation and defaults | 34 | # --- set test configutation and defaults |
| 35 | super().__init__({ # defaults | 35 | super().__init__({ # defaults |
| @@ -88,16 +88,14 @@ class TestFactory(dict): | @@ -88,16 +88,14 @@ class TestFactory(dict): | ||
| 88 | 88 | ||
| 89 | # make factory only for the questions used in the test | 89 | # make factory only for the questions used in the test |
| 90 | if q['ref'] in qrefs: | 90 | if q['ref'] in qrefs: |
| 91 | + q.setdefault('type', 'information') | ||
| 91 | q.update({ | 92 | q.update({ |
| 92 | 'filename': filename, | 93 | 'filename': filename, |
| 93 | 'path': dirname, | 94 | 'path': dirname, |
| 94 | 'index': i # position in the file, 0 based | 95 | 'index': i # position in the file, 0 based |
| 95 | }) | 96 | }) |
| 96 | 97 | ||
| 97 | - q.setdefault('type', 'information') | ||
| 98 | - | ||
| 99 | self.question_factory[q['ref']] = QFactory(q) | 98 | self.question_factory[q['ref']] = QFactory(q) |
| 100 | - logger.debug(f'[TestFactory.__init__] QFactory: "{q["ref"]}".') | ||
| 101 | 99 | ||
| 102 | # check if all the questions can be correctly generated | 100 | # check if all the questions can be correctly generated |
| 103 | try: | 101 | try: |
| @@ -245,17 +243,17 @@ class TestFactory(dict): | @@ -245,17 +243,17 @@ class TestFactory(dict): | ||
| 245 | # 'files': self['files'], | 243 | # 'files': self['files'], |
| 246 | }) | 244 | }) |
| 247 | 245 | ||
| 248 | - # ----------------------------------------------------------------------- | 246 | + # ------------------------------------------------------------------------ |
| 249 | def __repr__(self): | 247 | def __repr__(self): |
| 250 | testsettings = '\n'.join(f' {k:14s}: {v}' for k, v in self.items()) | 248 | testsettings = '\n'.join(f' {k:14s}: {v}' for k, v in self.items()) |
| 251 | return '{\n' + testsettings + '\n}' | 249 | return '{\n' + testsettings + '\n}' |
| 252 | 250 | ||
| 253 | 251 | ||
| 254 | -# =========================================================================== | 252 | +# ============================================================================ |
| 255 | # Each instance Test() is a concrete test of a single student. | 253 | # Each instance Test() is a concrete test of a single student. |
| 256 | -# =========================================================================== | 254 | +# ============================================================================ |
| 257 | class Test(dict): | 255 | class Test(dict): |
| 258 | - # ----------------------------------------------------------------------- | 256 | + # ------------------------------------------------------------------------ |
| 259 | def __init__(self, d): | 257 | def __init__(self, d): |
| 260 | super().__init__(d) | 258 | super().__init__(d) |
| 261 | self['start_time'] = datetime.now() | 259 | self['start_time'] = datetime.now() |
| @@ -263,20 +261,20 @@ class Test(dict): | @@ -263,20 +261,20 @@ class Test(dict): | ||
| 263 | self['state'] = 'ACTIVE' | 261 | self['state'] = 'ACTIVE' |
| 264 | self['comment'] = '' | 262 | self['comment'] = '' |
| 265 | 263 | ||
| 266 | - # ----------------------------------------------------------------------- | 264 | + # ------------------------------------------------------------------------ |
| 267 | # Removes all answers from the test (clean) | 265 | # Removes all answers from the test (clean) |
| 268 | def reset_answers(self): | 266 | def reset_answers(self): |
| 269 | for q in self['questions']: | 267 | for q in self['questions']: |
| 270 | q['answer'] = None | 268 | q['answer'] = None |
| 271 | 269 | ||
| 272 | - # ----------------------------------------------------------------------- | 270 | + # ------------------------------------------------------------------------ |
| 273 | # Given a dictionary ans={'ref': 'some answer'} updates the | 271 | # Given a dictionary ans={'ref': 'some answer'} updates the |
| 274 | # answers of the test. Only affects questions referred. | 272 | # answers of the test. Only affects questions referred. |
| 275 | def update_answers(self, ans): | 273 | def update_answers(self, ans): |
| 276 | for ref, answer in ans.items(): | 274 | for ref, answer in ans.items(): |
| 277 | self['questions'][ref]['answer'] = answer | 275 | self['questions'][ref]['answer'] = answer |
| 278 | 276 | ||
| 279 | - # ----------------------------------------------------------------------- | 277 | + # ------------------------------------------------------------------------ |
| 280 | # Corrects all the answers of the test and computes the final grade | 278 | # Corrects all the answers of the test and computes the final grade |
| 281 | async def correct(self): | 279 | async def correct(self): |
| 282 | self['finish_time'] = datetime.now() | 280 | self['finish_time'] = datetime.now() |
| @@ -290,7 +288,7 @@ class Test(dict): | @@ -290,7 +288,7 @@ class Test(dict): | ||
| 290 | self['grade'] = max(0, round(grade, 1)) # truncate negative grades | 288 | self['grade'] = max(0, round(grade, 1)) # truncate negative grades |
| 291 | return self['grade'] | 289 | return self['grade'] |
| 292 | 290 | ||
| 293 | - # ----------------------------------------------------------------------- | 291 | + # ------------------------------------------------------------------------ |
| 294 | def giveup(self): | 292 | def giveup(self): |
| 295 | self['finish_time'] = datetime.now() | 293 | self['finish_time'] = datetime.now() |
| 296 | self['state'] = 'QUIT' | 294 | self['state'] = 'QUIT' |