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