From 1d26fac2210c63fc7eb46239c9ddbc25969db2b7 Mon Sep 17 00:00:00 2001 From: Miguel Barão Date: Wed, 15 May 2019 16:41:35 +0100 Subject: [PATCH] - updates BUGS.md and README.md. - adds templates for loggers in config directory. --- BUGS.md | 24 ++++++++---------------- README.md | 35 +++++++++++++++-------------------- config/logger-debug.yaml | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ config/logger.yaml | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ perguntations/app.py | 16 ++++++++-------- perguntations/serve.py | 32 ++++++++++++++++---------------- perguntations/templates/test.html | 3 ++- perguntations/test.py | 30 ++++++++++++++---------------- 8 files changed, 173 insertions(+), 77 deletions(-) create mode 100644 config/logger-debug.yaml create mode 100644 config/logger.yaml diff --git a/BUGS.md b/BUGS.md index a29c0c0..8122644 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,9 +1,6 @@ # BUGS -- ensure pip nao funciona no ubuntu?... -- acrescentar logger.conf que sirva de base. -- exception sqlalchemy relacionada com threads. - quando se clica no texto de uma opcao, salta para outro lado na pagina. - ordenacao das notas em /admin nao é numerica, é ascii... - 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 @@ - melhorar o botao de autorizar (desliga-se), usar antes um botao? e.g. retornar None quando nao ha alteracoes relativamente à última vez. ou usar push (websockets?) -- pymips: nao pode executar syscalls do spim. -- perguntas checkbox [right,wrong] com pelo menos uma opção correcta. -- eventos unfocus? +- lidar com eventos unfocus. - servidor nao esta a lidar com eventos scroll/resize. ignorar? - Test.reset_answers() unused. # TODO +- test: mostrar duração do teste com progressbar no navbar. - submissao fazer um post ajax? -- fazer package para instalar perguntations com pip. - adicionar opcao para eliminar um teste em curso. -- gerar teste qd o prof autoriza? melhor nao, pode apagar o teste em curso. gerar previamente e manter uma pool de testes gerados? - enviar resposta de cada pergunta individualmente. - experimentar gerador de svg que inclua no markdown da pergunta e ver se funciona. -- 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. - quando ha varias perguntas para escolher, escolher sucessivamente em vez de aleatoriamente. - como refrescar a tabela de admin sem fazer reload da pagina? - 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?) - test: Cada pergunta respondida é logo submetida. - test: calculadora javascript. - admin: histograma das notas. -- 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??? - admin: mostrar as horas a que o teste terminou para os testes terminados. - admin: histograma das notas. - admin: mostrar teste gerado para aluno (tipo review). -- test: mostrar duração do teste com progressbar no navbar. -- fazer renderer para formulas com mathjax serverside (mathjax-node). +- fazer renderer para formulas com mathjax serverside (mathjax-node) ou usar katex. - fazer renderer para imagens, com links /file?ref=xpto;name=zzz.jpg - fazer renderer para linguagem assembly mips? -- permitir eliminar teste a decorrer - cancelar teste no menu admin. Dado o numero de aluno remove teste e faz logout do aluno. - mathjax-node: sudo pkg install node npm @@ -57,16 +47,18 @@ ou usar push (websockets?) - se ocorrer um erro na correcçao avisar aluno para contactar o professor. - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova? - 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) -- single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). - aviso na pagina principal para quem usa browser da treta - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput - perguntas para professor corrigir mais tarde. - fazer uma calculadora javascript e por no menu. surge como modal -- GeoIP? -- enviar logs para web? # FIXED +- 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. +- fazer package para instalar perguntations com pip. +- pymips: nao pode executar syscalls do spim. +- exception sqlalchemy relacionada com threads. +- acrescentar logger.conf que sirva de base. - 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 - textarea foi modificado em aprendizations para receber cmd line args. corrigir aqui tb. - usar npm para instalar javascript diff --git a/README.md b/README.md index 4dbb84c..400aaa9 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ ## Requirements -The webserver is a python application that requires `>=python3.6` and `pip` to be -installed. `npm` (Node package management) is also necessary to install the -javascript libraries. +The webserver is a python application that requires `>=python3.7` and `pip` to +be installed. Node package management `npm` is also necessary in order to +install the javascript libraries. ```bash -sudo apt install python3 python3-pip python3-setuptools npm # Ubuntu -sudo pkg install python36 py36-sqlite3 py36-pip py36-setuptools npm # FreeBSD +sudo apt install python3 python3-pip npm # Ubuntu +sudo pkg install python37 py37-sqlite3 py37-pip npm # FreeBSD sudo port install python37 py37-pip py37-setuptools npm6 # MacOS ``` @@ -38,19 +38,15 @@ This file is usually in `~/.config/pip/` in Linux and FreeBSD. In MacOS it's in ## Installation -Download and install (`USERNAME` is your account on bitbucket): +Download and install: ```bash -cd ~ -git clone https://USERNAME@bitbucket.org/USERNAME/perguntations.git +git clone https://git.xdi.uevora.pt/perguntations.git cd perguntations npm install pip3 install . ``` -Here, the repository is installed in the user home directory. -You may wish to adjust to somewhere else. - The command `npm` installs the javascript libraries and `pip3` installs the python webserver. 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 the following methods: ```bash -cd ~/perguntations/demo +cd perguntations/demo initdb students.csv # initialize from a CSV file initdb --admin # only adds the administrator account @@ -107,20 +103,20 @@ database. The database stores user passwords and grades, but not the actual tests. A test is specified in a single `yaml` file. -The demo already includes the `tutorial.yaml` that you can play with. +The demo already includes the `demo.yaml` that you can play with. The complete tests submitted by the students are stored in JSON files in the -directory defined in `tutorial.yaml` under the option `answers_dir: ans`. +directory defined in `demo.yaml` under the option `answers_dir: ans`. We also have to create this directory manually: ```bash mkdir ans # directory where the tests will be saved ``` -Start the server and run the `tutorial.yaml` test: +Start the server and run the `demo.yaml` test: ```bash -perguntations tutorial.yaml # run demo test +perguntations demo.yaml # run demo test ``` Several options are available, run `perguntations --help` for a list. @@ -205,7 +201,7 @@ sudo service pf start # enable pf firewall ``` Certificates are saved in `/usr/local/etc/letsencrypt/live/www.example.com/`. -Copy them to the `certs` directory and change permissions: +Copy them to the `~/.local/share/certs` directory and change permissions: ```sh chmod 400 cert.pem privkey.pem @@ -219,15 +215,14 @@ sudo certbot renew sudo service pf start # start firewall ``` -Again, copy certificate files `privkey.pem` and `cert.pem` to the `certs` -directory. +Again, copy certificate files `privkey.pem` and `cert.pem` to `~/.local/share/certs`. --- ## Troubleshooting - The server tries to run `python3` so this command must be accessible from -user accounts. Currently, the minimum supported python version is 3.6. +user accounts. Currently, the minimum supported python version is 3.7. - If you are getting any `UnicodeEncodeError` type of errors that's because the terminal is not supporting UTF-8. diff --git a/config/logger-debug.yaml b/config/logger-debug.yaml new file mode 100644 index 0000000..e546152 --- /dev/null +++ b/config/logger-debug.yaml @@ -0,0 +1,59 @@ +--- +version: 1 + +formatters: + void: + format: '' + standard: + format: '%(asctime)s | %(levelname)-8s | %(module)-16s:%(lineno)4d | %(thread)d | %(message)s' + + error: + format: "\e[41m%(asctime)s | %(levelname)-8s | %(module)-16s:%(lineno)4d | %(thread)d | %(message)s\e[0m" + +handlers: + default: + level: 'DEBUG' + class: 'logging.StreamHandler' + formatter: 'standard' + stream: 'ext://sys.stdout' + + error: + level: 'ERROR' + class: 'logging.StreamHandler' + formatter: 'error' + stream: 'ext://sys.stdout' + +loggers: + '': + handlers: ['default', 'error'] + level: 'DEBUG' + + 'perguntations.app': + handlers: ['default', 'error'] + level: 'DEBUG' + propagate: false + + 'perguntations.models': + handlers: ['default', 'error'] + level: 'DEBUG' + propagate: false + + 'perguntations.questions': + handlers: ['default', 'error'] + level: 'DEBUG' + propagate: false + + 'perguntations.test': + handlers: ['default', 'error'] + level: 'DEBUG' + propagate: false + + 'perguntations.tools': + handlers: ['default', 'error'] + level: 'DEBUG' + propagate: false + + 'perguntations.parser_markdown': + handlers: ['default', 'error'] + level: 'DEBUG' + propagate: false diff --git a/config/logger.yaml b/config/logger.yaml new file mode 100644 index 0000000..652e7ac --- /dev/null +++ b/config/logger.yaml @@ -0,0 +1,51 @@ +--- +version: 1 + +formatters: + void: + format: '' + standard: + format: '%(asctime)s | %(levelname)-8s | %(message)s' + datefmt: '%Y-%m-%d %H:%M:%S' + +handlers: + default: + level: 'INFO' + class: 'logging.StreamHandler' + formatter: 'standard' + stream: 'ext://sys.stdout' + +loggers: + '': + handlers: ['default'] + level: 'INFO' + + 'perguntations.app': + handlers: ['default'] + level: 'INFO' + propagate: false + + 'perguntations.models': + handlers: ['default'] + level: 'INFO' + propagate: false + + 'perguntations.questions': + handlers: ['default'] + level: 'INFO' + propagate: false + + 'perguntations.test': + handlers: ['default'] + level: 'INFO' + propagate: false + + 'perguntations.tools': + handlers: ['default'] + level: 'INFO' + propagate: false + + 'perguntations.parser_markdown': + handlers: ['default'] + level: 'INFO' + propagate: false diff --git a/perguntations/app.py b/perguntations/app.py index c3fd08f..c329f88 100644 --- a/perguntations/app.py +++ b/perguntations/app.py @@ -45,7 +45,7 @@ async def hash_password(pw): # Application # ============================================================================ class App(object): - # ----------------------------------------------------------------------- + # ------------------------------------------------------------------------ # helper to manage db sessions using the `with` statement, for example # with self.db_session() as s: s.query(...) @contextmanager @@ -98,14 +98,14 @@ class App(object): for student in self.get_all_students(): self.allow_student(student[0]) - # ----------------------------------------------------------------------- + # ------------------------------------------------------------------------ def exit(self): if len(self.online) > 1: online_students = ', '.join(self.online) logger.warning(f'Students still online: {online_students}') logger.critical('----------- !!! Server terminated !!! -----------') - # ----------------------------------------------------------------------- + # ------------------------------------------------------------------------ async def login(self, uid, try_pw): if uid not in self.allowed and uid != '0': # not allowed logger.warning(f'Student {uid}: not allowed to login.') @@ -136,12 +136,12 @@ class App(object): logger.info(f'Student {uid}: wrong password.') return False - # ----------------------------------------------------------------------- + # ------------------------------------------------------------------------ def logout(self, uid): self.online.pop(uid, None) # remove from dict if exists logger.info(f'Student {uid}: logged out.') - # ----------------------------------------------------------------------- + # ------------------------------------------------------------------------ async def generate_test(self, uid): if uid in self.online: logger.info(f'Student {uid}: generating new test.') @@ -154,7 +154,7 @@ class App(object): # this implies an error in the code. should never be here! logger.critical(f'Student {uid}: offline, can\'t generate test') - # ----------------------------------------------------------------------- + # ------------------------------------------------------------------------ # ans is a dictionary {question_index: answer, ...} # for example: {0:'hello', 1:[1,2]} async def correct_test(self, uid, ans): @@ -199,7 +199,7 @@ class App(object): logger.info(f'Student {uid}: database updated.') return grade - # ----------------------------------------------------------------------- + # ------------------------------------------------------------------------ def giveup_test(self, uid): t = self.online[uid]['test'] t.giveup() @@ -226,7 +226,7 @@ class App(object): logger.info(f'Student {uid}: gave up.') return t - # ----------------------------------------------------------------------- + # ------------------------------------------------------------------------ # --- helpers (getters) # def get_student_name(self, uid): diff --git a/perguntations/serve.py b/perguntations/serve.py index 3d14c31..60dff56 100644 --- a/perguntations/serve.py +++ b/perguntations/serve.py @@ -117,7 +117,6 @@ class LogoutHandler(BaseHandler): # ---------------------------------------------------------------------------- # handles root / to redirect students to /test and admininistrator to /admin # ---------------------------------------------------------------------------- -# TODO list available tests class RootHandler(BaseHandler): @tornado.web.authenticated def get(self): @@ -168,9 +167,9 @@ class FileHandler(BaseHandler): break # for loop -# ------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Test shown to students -# ------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- class TestHandler(BaseHandler): _templates = { 'radio': 'question-radio.html', @@ -190,7 +189,7 @@ class TestHandler(BaseHandler): @tornado.web.authenticated async def get(self): uid = self.current_user - t = self.testapp.get_student_test(uid) # reload page returns same test + t = self.testapp.get_student_test(uid) # reloading returns same test if t is None: t = await self.testapp.generate_test(uid) self.render('test.html', t=t, md=md_to_html, templ=self._templates) @@ -206,11 +205,11 @@ class TestHandler(BaseHandler): t = self.testapp.get_student_test(uid) ans = {} for i, q in enumerate(t['questions']): - qid = str(i) # question id + qid = str(i) if 'answered-' + qid in self.request.arguments: ans[i] = self.get_body_arguments(qid) - # remove list when it does not make sense... + # remove enclosing list in some question types if q['type'] == 'radio': if not ans[i]: ans[i] = None @@ -220,17 +219,18 @@ class TestHandler(BaseHandler): 'numeric-interval'): ans[i] = ans[i][0] + # correct answered questions and logout await self.testapp.correct_test(uid, ans) - self.testapp.logout(uid) self.clear_cookie('user') + # show final grade and grades of other tests in the database allgrades = self.testapp.get_student_grades_from_all_tests(uid) self.render('grade.html', t=t, allgrades=allgrades) -# ------------------------------------------------------------------------- -# FIXME this should be a post in the test with command giveup instead of correct... +# ---------------------------------------------------------------------------- +# FIXME should be a post in the test with command giveup instead of correct... # class GiveupHandler(BaseHandler): # @tornado.web.authenticated # def get(self): @@ -242,7 +242,7 @@ class TestHandler(BaseHandler): # self.render('grade.html', t=t, allgrades=self.testapp.get_student_grades_from_all_tests(uid)) -# --- REVIEW ------------------------------------------------------------- +# --- REVIEW ----------------------------------------------------------------- class ReviewHandler(BaseHandler): SUPPORTED_METHODS = ['GET'] @@ -330,7 +330,7 @@ class AdminHandler(BaseHandler): logging.error(f'Unknown command: "{cmd}"') -# ------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- def signal_handler(signal, frame): r = input(' --> Stop webserver? (yes/no) ') if r in ('yes', 'YES'): @@ -339,7 +339,7 @@ def signal_handler(signal, frame): sys.exit(0) -# ------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- def parse_cmdline_arguments(): parser = argparse.ArgumentParser( description='Server for online tests. Enrolled students and tests ' @@ -363,7 +363,7 @@ def parse_cmdline_arguments(): return parser.parse_args() -# ------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- def get_logger_config(debug=False): if debug: filename = 'logger-debug.yaml' @@ -409,9 +409,9 @@ def get_logger_config(debug=False): return load_yaml(config_file, default=default_config) -# ------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Tornado web server -# ------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- def main(): args = parse_cmdline_arguments() @@ -477,6 +477,6 @@ def main(): raise -# ------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- if __name__ == "__main__": main() diff --git a/perguntations/templates/test.html b/perguntations/templates/test.html index 13a012d..607bffe 100644 --- a/perguntations/templates/test.html +++ b/perguntations/templates/test.html @@ -120,7 +120,8 @@