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