Commit 1d26fac2210c63fc7eb46239c9ddbc25969db2b7

Authored by Miguel Barão
1 parent 71a43de2
Exists in master and in 1 other branch dev

- updates BUGS.md and README.md.

- adds templates for loggers in config directory.
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
@@ -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&#39;s in @@ -38,19 +38,15 @@ This file is usually in `~/.config/pip/` in Linux and FreeBSD. In MacOS it&#39;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.
config/logger-debug.yaml 0 → 100644
@@ -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
config/logger.yaml 0 → 100644
@@ -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'