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 | 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 | 4 | - quando se clica no texto de uma opcao, salta para outro lado na pagina. |
8 | 5 | - ordenacao das notas em /admin nao é numerica, é ascii... |
9 | 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 | 9 | - melhorar o botao de autorizar (desliga-se), usar antes um botao? |
13 | 10 | e.g. retornar None quando nao ha alteracoes relativamente à última vez. |
14 | 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 | 13 | - servidor nao esta a lidar com eventos scroll/resize. ignorar? |
19 | 14 | - Test.reset_answers() unused. |
20 | 15 | |
21 | 16 | # TODO |
22 | 17 | |
18 | +- test: mostrar duração do teste com progressbar no navbar. | |
23 | 19 | - submissao fazer um post ajax? |
24 | -- fazer package para instalar perguntations com pip. | |
25 | 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 | 21 | - enviar resposta de cada pergunta individualmente. |
28 | 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 | 23 | - quando ha varias perguntas para escolher, escolher sucessivamente em vez de aleatoriamente. |
31 | 24 | - como refrescar a tabela de admin sem fazer reload da pagina? |
32 | 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 | 27 | - test: Cada pergunta respondida é logo submetida. |
35 | 28 | - test: calculadora javascript. |
36 | 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 | 30 | - admin: mostrar as horas a que o teste terminou para os testes terminados. |
39 | 31 | - admin: histograma das notas. |
40 | 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 | 34 | - fazer renderer para imagens, com links /file?ref=xpto;name=zzz.jpg |
44 | 35 | - fazer renderer para linguagem assembly mips? |
45 | -- permitir eliminar teste a decorrer | |
46 | 36 | - cancelar teste no menu admin. Dado o numero de aluno remove teste e faz logout do aluno. |
47 | 37 | - mathjax-node: |
48 | 38 | sudo pkg install node npm |
... | ... | @@ -57,16 +47,18 @@ ou usar push (websockets?) |
57 | 47 | - se ocorrer um erro na correcçao avisar aluno para contactar o professor. |
58 | 48 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova? |
59 | 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 | 50 | - aviso na pagina principal para quem usa browser da treta |
62 | 51 | - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput |
63 | 52 | - perguntas para professor corrigir mais tarde. |
64 | 53 | - fazer uma calculadora javascript e por no menu. surge como modal |
65 | -- GeoIP? | |
66 | -- enviar logs para web? | |
67 | 54 | |
68 | 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 | 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 | 63 | - textarea foi modificado em aprendizations para receber cmd line args. corrigir aqui tb. |
72 | 64 | - usar npm para instalar javascript | ... | ... |
README.md
... | ... | @@ -11,13 +11,13 @@ |
11 | 11 | |
12 | 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 | 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 | 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 | 38 | |
39 | 39 | ## Installation |
40 | 40 | |
41 | -Download and install (`USERNAME` is your account on bitbucket): | |
41 | +Download and install: | |
42 | 42 | |
43 | 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 | 45 | cd perguntations |
47 | 46 | npm install |
48 | 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 | 50 | The command `npm` installs the javascript libraries and `pip3` installs the |
55 | 51 | python webserver. |
56 | 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 | 91 | the following methods: |
96 | 92 | |
97 | 93 | ```bash |
98 | -cd ~/perguntations/demo | |
94 | +cd perguntations/demo | |
99 | 95 | |
100 | 96 | initdb students.csv # initialize from a CSV file |
101 | 97 | initdb --admin # only adds the administrator account |
... | ... | @@ -107,20 +103,20 @@ database. |
107 | 103 | The database stores user passwords and grades, but not the actual tests. |
108 | 104 | |
109 | 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 | 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 | 110 | We also have to create this directory manually: |
115 | 111 | |
116 | 112 | ```bash |
117 | 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 | 118 | ```bash |
123 | -perguntations tutorial.yaml # run demo test | |
119 | +perguntations demo.yaml # run demo test | |
124 | 120 | ``` |
125 | 121 | |
126 | 122 | Several options are available, run `perguntations --help` for a list. |
... | ... | @@ -205,7 +201,7 @@ sudo service pf start # enable pf firewall |
205 | 201 | ``` |
206 | 202 | |
207 | 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 | 206 | ```sh |
211 | 207 | chmod 400 cert.pem privkey.pem |
... | ... | @@ -219,15 +215,14 @@ sudo certbot renew |
219 | 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 | 222 | ## Troubleshooting |
228 | 223 | |
229 | 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 | 227 | - If you are getting any `UnicodeEncodeError` type of errors that's because the |
233 | 228 | terminal is not supporting UTF-8. | ... | ... |
... | ... | @@ -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 @@ |
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 | 45 | # Application |
46 | 46 | # ============================================================================ |
47 | 47 | class App(object): |
48 | - # ----------------------------------------------------------------------- | |
48 | + # ------------------------------------------------------------------------ | |
49 | 49 | # helper to manage db sessions using the `with` statement, for example |
50 | 50 | # with self.db_session() as s: s.query(...) |
51 | 51 | @contextmanager |
... | ... | @@ -98,14 +98,14 @@ class App(object): |
98 | 98 | for student in self.get_all_students(): |
99 | 99 | self.allow_student(student[0]) |
100 | 100 | |
101 | - # ----------------------------------------------------------------------- | |
101 | + # ------------------------------------------------------------------------ | |
102 | 102 | def exit(self): |
103 | 103 | if len(self.online) > 1: |
104 | 104 | online_students = ', '.join(self.online) |
105 | 105 | logger.warning(f'Students still online: {online_students}') |
106 | 106 | logger.critical('----------- !!! Server terminated !!! -----------') |
107 | 107 | |
108 | - # ----------------------------------------------------------------------- | |
108 | + # ------------------------------------------------------------------------ | |
109 | 109 | async def login(self, uid, try_pw): |
110 | 110 | if uid not in self.allowed and uid != '0': # not allowed |
111 | 111 | logger.warning(f'Student {uid}: not allowed to login.') |
... | ... | @@ -136,12 +136,12 @@ class App(object): |
136 | 136 | logger.info(f'Student {uid}: wrong password.') |
137 | 137 | return False |
138 | 138 | |
139 | - # ----------------------------------------------------------------------- | |
139 | + # ------------------------------------------------------------------------ | |
140 | 140 | def logout(self, uid): |
141 | 141 | self.online.pop(uid, None) # remove from dict if exists |
142 | 142 | logger.info(f'Student {uid}: logged out.') |
143 | 143 | |
144 | - # ----------------------------------------------------------------------- | |
144 | + # ------------------------------------------------------------------------ | |
145 | 145 | async def generate_test(self, uid): |
146 | 146 | if uid in self.online: |
147 | 147 | logger.info(f'Student {uid}: generating new test.') |
... | ... | @@ -154,7 +154,7 @@ class App(object): |
154 | 154 | # this implies an error in the code. should never be here! |
155 | 155 | logger.critical(f'Student {uid}: offline, can\'t generate test') |
156 | 156 | |
157 | - # ----------------------------------------------------------------------- | |
157 | + # ------------------------------------------------------------------------ | |
158 | 158 | # ans is a dictionary {question_index: answer, ...} |
159 | 159 | # for example: {0:'hello', 1:[1,2]} |
160 | 160 | async def correct_test(self, uid, ans): |
... | ... | @@ -199,7 +199,7 @@ class App(object): |
199 | 199 | logger.info(f'Student {uid}: database updated.') |
200 | 200 | return grade |
201 | 201 | |
202 | - # ----------------------------------------------------------------------- | |
202 | + # ------------------------------------------------------------------------ | |
203 | 203 | def giveup_test(self, uid): |
204 | 204 | t = self.online[uid]['test'] |
205 | 205 | t.giveup() |
... | ... | @@ -226,7 +226,7 @@ class App(object): |
226 | 226 | logger.info(f'Student {uid}: gave up.') |
227 | 227 | return t |
228 | 228 | |
229 | - # ----------------------------------------------------------------------- | |
229 | + # ------------------------------------------------------------------------ | |
230 | 230 | |
231 | 231 | # --- helpers (getters) |
232 | 232 | # def get_student_name(self, uid): | ... | ... |
perguntations/serve.py
... | ... | @@ -117,7 +117,6 @@ class LogoutHandler(BaseHandler): |
117 | 117 | # ---------------------------------------------------------------------------- |
118 | 118 | # handles root / to redirect students to /test and admininistrator to /admin |
119 | 119 | # ---------------------------------------------------------------------------- |
120 | -# TODO list available tests | |
121 | 120 | class RootHandler(BaseHandler): |
122 | 121 | @tornado.web.authenticated |
123 | 122 | def get(self): |
... | ... | @@ -168,9 +167,9 @@ class FileHandler(BaseHandler): |
168 | 167 | break # for loop |
169 | 168 | |
170 | 169 | |
171 | -# ------------------------------------------------------------------------- | |
170 | +# ---------------------------------------------------------------------------- | |
172 | 171 | # Test shown to students |
173 | -# ------------------------------------------------------------------------- | |
172 | +# ---------------------------------------------------------------------------- | |
174 | 173 | class TestHandler(BaseHandler): |
175 | 174 | _templates = { |
176 | 175 | 'radio': 'question-radio.html', |
... | ... | @@ -190,7 +189,7 @@ class TestHandler(BaseHandler): |
190 | 189 | @tornado.web.authenticated |
191 | 190 | async def get(self): |
192 | 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 | 193 | if t is None: |
195 | 194 | t = await self.testapp.generate_test(uid) |
196 | 195 | self.render('test.html', t=t, md=md_to_html, templ=self._templates) |
... | ... | @@ -206,11 +205,11 @@ class TestHandler(BaseHandler): |
206 | 205 | t = self.testapp.get_student_test(uid) |
207 | 206 | ans = {} |
208 | 207 | for i, q in enumerate(t['questions']): |
209 | - qid = str(i) # question id | |
208 | + qid = str(i) | |
210 | 209 | if 'answered-' + qid in self.request.arguments: |
211 | 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 | 213 | if q['type'] == 'radio': |
215 | 214 | if not ans[i]: |
216 | 215 | ans[i] = None |
... | ... | @@ -220,17 +219,18 @@ class TestHandler(BaseHandler): |
220 | 219 | 'numeric-interval'): |
221 | 220 | ans[i] = ans[i][0] |
222 | 221 | |
222 | + # correct answered questions and logout | |
223 | 223 | await self.testapp.correct_test(uid, ans) |
224 | - | |
225 | 224 | self.testapp.logout(uid) |
226 | 225 | self.clear_cookie('user') |
227 | 226 | |
227 | + # show final grade and grades of other tests in the database | |
228 | 228 | allgrades = self.testapp.get_student_grades_from_all_tests(uid) |
229 | 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 | 234 | # class GiveupHandler(BaseHandler): |
235 | 235 | # @tornado.web.authenticated |
236 | 236 | # def get(self): |
... | ... | @@ -242,7 +242,7 @@ class TestHandler(BaseHandler): |
242 | 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 | 246 | class ReviewHandler(BaseHandler): |
247 | 247 | SUPPORTED_METHODS = ['GET'] |
248 | 248 | |
... | ... | @@ -330,7 +330,7 @@ class AdminHandler(BaseHandler): |
330 | 330 | logging.error(f'Unknown command: "{cmd}"') |
331 | 331 | |
332 | 332 | |
333 | -# ------------------------------------------------------------------------- | |
333 | +# ---------------------------------------------------------------------------- | |
334 | 334 | def signal_handler(signal, frame): |
335 | 335 | r = input(' --> Stop webserver? (yes/no) ') |
336 | 336 | if r in ('yes', 'YES'): |
... | ... | @@ -339,7 +339,7 @@ def signal_handler(signal, frame): |
339 | 339 | sys.exit(0) |
340 | 340 | |
341 | 341 | |
342 | -# ------------------------------------------------------------------------- | |
342 | +# ---------------------------------------------------------------------------- | |
343 | 343 | def parse_cmdline_arguments(): |
344 | 344 | parser = argparse.ArgumentParser( |
345 | 345 | description='Server for online tests. Enrolled students and tests ' |
... | ... | @@ -363,7 +363,7 @@ def parse_cmdline_arguments(): |
363 | 363 | return parser.parse_args() |
364 | 364 | |
365 | 365 | |
366 | -# ------------------------------------------------------------------------- | |
366 | +# ---------------------------------------------------------------------------- | |
367 | 367 | def get_logger_config(debug=False): |
368 | 368 | if debug: |
369 | 369 | filename = 'logger-debug.yaml' |
... | ... | @@ -409,9 +409,9 @@ def get_logger_config(debug=False): |
409 | 409 | return load_yaml(config_file, default=default_config) |
410 | 410 | |
411 | 411 | |
412 | -# ------------------------------------------------------------------------- | |
412 | +# ---------------------------------------------------------------------------- | |
413 | 413 | # Tornado web server |
414 | -# ------------------------------------------------------------------------- | |
414 | +# ---------------------------------------------------------------------------- | |
415 | 415 | def main(): |
416 | 416 | args = parse_cmdline_arguments() |
417 | 417 | |
... | ... | @@ -477,6 +477,6 @@ def main(): |
477 | 477 | raise |
478 | 478 | |
479 | 479 | |
480 | -# ------------------------------------------------------------------------- | |
480 | +# ---------------------------------------------------------------------------- | |
481 | 481 | if __name__ == "__main__": |
482 | 482 | main() | ... | ... |
perguntations/templates/test.html
... | ... | @@ -120,7 +120,8 @@ |
120 | 120 | </div> |
121 | 121 | <div class="modal-body"> |
122 | 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 | 125 | </div> |
125 | 126 | <div class="modal-footer"> |
126 | 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 | 14 | logger = logging.getLogger(__name__) |
15 | 15 | |
16 | 16 | |
17 | -# =========================================================================== | |
17 | +# ============================================================================ | |
18 | 18 | class TestFactoryException(Exception): |
19 | 19 | pass |
20 | 20 | |
21 | 21 | |
22 | -# =========================================================================== | |
22 | +# ============================================================================ | |
23 | 23 | # Each instance of TestFactory() is a test generator. |
24 | 24 | # For example, if we want to serve two different tests, then we need two |
25 | 25 | # instances of TestFactory(), one for each test. |
26 | -# =========================================================================== | |
26 | +# ============================================================================ | |
27 | 27 | class TestFactory(dict): |
28 | - # ----------------------------------------------------------------------- | |
28 | + # ------------------------------------------------------------------------ | |
29 | 29 | # Loads configuration from yaml file, then overrides some configurations |
30 | 30 | # using the conf argument. |
31 | 31 | # Base questions are added to a pool of questions factories. |
32 | - # ----------------------------------------------------------------------- | |
32 | + # ------------------------------------------------------------------------ | |
33 | 33 | def __init__(self, conf): |
34 | 34 | # --- set test configutation and defaults |
35 | 35 | super().__init__({ # defaults |
... | ... | @@ -88,16 +88,14 @@ class TestFactory(dict): |
88 | 88 | |
89 | 89 | # make factory only for the questions used in the test |
90 | 90 | if q['ref'] in qrefs: |
91 | + q.setdefault('type', 'information') | |
91 | 92 | q.update({ |
92 | 93 | 'filename': filename, |
93 | 94 | 'path': dirname, |
94 | 95 | 'index': i # position in the file, 0 based |
95 | 96 | }) |
96 | 97 | |
97 | - q.setdefault('type', 'information') | |
98 | - | |
99 | 98 | self.question_factory[q['ref']] = QFactory(q) |
100 | - logger.debug(f'[TestFactory.__init__] QFactory: "{q["ref"]}".') | |
101 | 99 | |
102 | 100 | # check if all the questions can be correctly generated |
103 | 101 | try: |
... | ... | @@ -245,17 +243,17 @@ class TestFactory(dict): |
245 | 243 | # 'files': self['files'], |
246 | 244 | }) |
247 | 245 | |
248 | - # ----------------------------------------------------------------------- | |
246 | + # ------------------------------------------------------------------------ | |
249 | 247 | def __repr__(self): |
250 | 248 | testsettings = '\n'.join(f' {k:14s}: {v}' for k, v in self.items()) |
251 | 249 | return '{\n' + testsettings + '\n}' |
252 | 250 | |
253 | 251 | |
254 | -# =========================================================================== | |
252 | +# ============================================================================ | |
255 | 253 | # Each instance Test() is a concrete test of a single student. |
256 | -# =========================================================================== | |
254 | +# ============================================================================ | |
257 | 255 | class Test(dict): |
258 | - # ----------------------------------------------------------------------- | |
256 | + # ------------------------------------------------------------------------ | |
259 | 257 | def __init__(self, d): |
260 | 258 | super().__init__(d) |
261 | 259 | self['start_time'] = datetime.now() |
... | ... | @@ -263,20 +261,20 @@ class Test(dict): |
263 | 261 | self['state'] = 'ACTIVE' |
264 | 262 | self['comment'] = '' |
265 | 263 | |
266 | - # ----------------------------------------------------------------------- | |
264 | + # ------------------------------------------------------------------------ | |
267 | 265 | # Removes all answers from the test (clean) |
268 | 266 | def reset_answers(self): |
269 | 267 | for q in self['questions']: |
270 | 268 | q['answer'] = None |
271 | 269 | |
272 | - # ----------------------------------------------------------------------- | |
270 | + # ------------------------------------------------------------------------ | |
273 | 271 | # Given a dictionary ans={'ref': 'some answer'} updates the |
274 | 272 | # answers of the test. Only affects questions referred. |
275 | 273 | def update_answers(self, ans): |
276 | 274 | for ref, answer in ans.items(): |
277 | 275 | self['questions'][ref]['answer'] = answer |
278 | 276 | |
279 | - # ----------------------------------------------------------------------- | |
277 | + # ------------------------------------------------------------------------ | |
280 | 278 | # Corrects all the answers of the test and computes the final grade |
281 | 279 | async def correct(self): |
282 | 280 | self['finish_time'] = datetime.now() |
... | ... | @@ -290,7 +288,7 @@ class Test(dict): |
290 | 288 | self['grade'] = max(0, round(grade, 1)) # truncate negative grades |
291 | 289 | return self['grade'] |
292 | 290 | |
293 | - # ----------------------------------------------------------------------- | |
291 | + # ------------------------------------------------------------------------ | |
294 | 292 | def giveup(self): |
295 | 293 | self['finish_time'] = datetime.now() |
296 | 294 | self['state'] = 'QUIT' | ... | ... |