diff --git a/BUGS.md b/BUGS.md index 44e22af..246f39b 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,11 +1,15 @@ BUGS: +- se students.db não existe, rebenta. +- textarea tem codigo para preencher o texto, mas ja não é necessário porque pergunta não é reloaded. - questions hardcoded in LearnApp. - database hardcoded in LearnApp. - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() TODO: +- configuração e linha de comando. +- logging - como gerar uma sequencia de perguntas? - generators not working: bcrypt (ver blog) - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. diff --git a/README.md b/README.md index b7ebd7c..7d1d673 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,31 @@ +# Get Started + + ## Requirements -You will need to install `python3.6`, `pip` and `sqlite3`. -This can be done using the system package management, downloaded from [http://www.python.org](), or compiled from sources. In the end you should be able to run `pip3 --version` and `python3 -c "import sqlite3"` without errors (sometimes `pip3` is `pip`, `pip3.6` or `pip-3.6`). +We will need to install python3.6, pip and sqlite3 python package. +This can be done using the system package management, downloaded from [http://www.python.org](), or compiled from sources. + +- Installing from the system package management: + - OSX: `port install python36` + - FreeBSD: `pkg install python36 py36-sqlite3` + - Linux: `apt-get install ???` +- Installing from sources: + - Download from [http://www.python.org]() + - `unxz Python-3.6.tar.xz` + - `tar xvf Python-3.6.tar` + - `cd Python-3.6` + - `./configure --prefix=$HOME/.local/bin` + - `make && make install` + +To install pip (if not yet installed): + + python36 -m ensurepip --user -Install some python packages locally on the user area: +This will install pip in your account under `~/.local/bin`. +In the end you should be able to run `pip3 --version` and `python3 -c "import sqlite3"` without errors (sometimes `pip3` is `pip`, `pip3.6` or `pip-3.6` are used). + +Install additional python packages locally on the user area: pip install --user tornado sqlalchemy pyyaml pygments markdown bcrypt @@ -12,6 +34,12 @@ These are usually installed under - OSX: `~/Library/python/3.6/lib/python/site-packages/` - Linux/FreeBSD: `~/.local/lib/python3.6/site-packages/` +Note: If you want to always install python modules on the user account, edit the pip configuration file `~/.config/pip/pip.conf` (FreeBSD, Linux) or `Library/Application Support/pip/pip.conf` (OSX) and add the lines + + [global] + user = yes + + ## Installation Replace USER by your bitbucket username: @@ -30,7 +58,7 @@ First we need to create a database: ./initdb.py # initialize with a single user `0` and empty password ./initdb.py --help # for the available options -We also need certificates for https. We can generate selfsigned certificates using openssl: +We also need certificates for https. Generate selfsigned certificates using openssl: mkdir certs cd certs diff --git a/app.py b/app.py index d0a464b..d4f6f98 100644 --- a/app.py +++ b/app.py @@ -2,16 +2,24 @@ import random from contextlib import contextmanager # `with` statement in db sessions from datetime import datetime +import logging -# libs -import bcrypt -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker #, scoped_session +# user installed libraries +try: + import bcrypt + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker +except ImportError: + logger.critical('Python package missing. See README.md for instructions.') + sys.exit(1) # this project import questions from models import Student, Answer +# setup logger for this module +logger = logging.getLogger(__name__) + # ============================================================================ # LearnApp - application logic # ============================================================================ @@ -30,21 +38,23 @@ class LearnApp(object): with self.db_session() as s: n = s.query(Student).count() # filter(Student.id != '0'). except Exception as e: - print('Database not usable.') + logger.critical('Database not usable.') raise e else: - print('Database has {} students registered.'.format(n)) + logger.info(f'Database has {n} students registered.') # ------------------------------------------------------------------------ def login(self, uid, try_pw): with self.db_session() as s: student = s.query(Student).filter(Student.id == uid).one_or_none() - if student is None or student in self.online: # FIXME + if student is None: + logger.info(f'User "{uid}" does not exist.') return False # student does not exist or already loggeg in hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) if hashedtry != student.password: + logger.info(f'User "{uid}" wrong password.') return False # wrong password # success diff --git a/certs/.gitignore b/certs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/certs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/config/logger.yaml b/config/logger.yaml new file mode 100644 index 0000000..cb24382 --- /dev/null +++ b/config/logger.yaml @@ -0,0 +1,36 @@ + +version: 1 + +formatters: + void: + format: '' + standard: + format: '%(asctime)s | %(levelname)-8s | %(name)-14s | %(message)s' + +handlers: + default: + level: 'INFO' + class: 'logging.StreamHandler' + formatter: 'standard' + stream: 'ext://sys.stdout' + +loggers: + '': + handlers: ['default'] + level: 'INFO' + + 'app': + handlers: ['default'] + level: 'INFO' + propagate: False + + 'questions': + handlers: ['default'] + level: 'INFO' + propagate: False + + 'tools': + handlers: ['default'] + level: 'INFO' + propagate: False + diff --git a/serve.py b/serve.py index 1bf1978..866f42a 100755 --- a/serve.py +++ b/serve.py @@ -2,34 +2,30 @@ # python standard library import os +import sys import json import base64 import uuid - -# installed libraries -import markdown -import tornado.ioloop -import tornado.web -import tornado.httpserver -from tornado import template, gen import concurrent.futures +import logging.config + +# user installed libraries +try: + import markdown + import tornado.ioloop + import tornado.web + import tornado.httpserver + from tornado import template, gen +except ImportError: + print('Some python packages are missing. See README.md for instructions.') + sys.exit(1) # this project from app import LearnApp - -# markdown helper -def md(text): - return markdown.markdown(text, - extensions=[ - 'markdown.extensions.tables', - 'markdown.extensions.fenced_code', - 'markdown.extensions.codehilite', - 'markdown.extensions.def_list', - 'markdown.extensions.sane_lists' - ]) +from tools import load_yaml, md # A thread pool to be used for password hashing with bcrypt. FIXME and other things? -executor = concurrent.futures.ThreadPoolExecutor(2) +# executor = concurrent.futures.ThreadPoolExecutor(2) # ============================================================================ @@ -89,11 +85,11 @@ class LoginHandler(BaseHandler): # print(f'login.post: user={uid}, pw={pw}') if self.learn.login(uid, pw): - print('login ok') + logging.info(f'User "{uid}" login ok.') self.set_secure_cookie("user", str(uid), expires_days=30) self.redirect(self.get_argument("next", "/")) else: - print('login failed') + logging.info(f'User "{uid}" login failed.') self.render("login.html", error='Número ou senha incorrectos') # ---------------------------------------------------------------------------- @@ -159,19 +155,40 @@ class QuestionHandler(BaseHandler): # ---------------------------------------------------------------------------- def main(): - webapp = WebApplication() + SERVER_PATH = os.path.dirname(os.path.realpath(__file__)) + LOGGER_CONF = os.path.join(SERVER_PATH, 'config/logger.yaml') + + + # --- Setup logging + try: + logging.config.dictConfig(load_yaml(LOGGER_CONF)) + except: # FIXME should this be done in a different way? + print('An error ocurred while setting up the logging system.') + print('Common causes:\n - inexistent directory "logs"?\n - write permission to "logs" directory?') + sys.exit(1) + + # --- start application + try: + webapp = WebApplication() + except: + logging.critical('Can\'t start application.') + sys.exit(1) + + # --- create webserver http_server = tornado.httpserver.HTTPServer(webapp, ssl_options={ "certfile": "certs/cert.pem", "keyfile": "certs/key.pem" }) http_server.listen(8443) + # --- start webserver try: - print('--- start ---') + logging.info('Webserver running...') tornado.ioloop.IOLoop.current().start() + # running... except KeyboardInterrupt: tornado.ioloop.IOLoop.current().stop() - print('\n--- stop ---') + logging.info('Webserver stopped.') # ---------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/tools.py b/tools.py index ccccec2..0f43a07 100644 --- a/tools.py +++ b/tools.py @@ -55,27 +55,60 @@ def run_script(script, stdin='', timeout=5): else: return output -def md_to_html(text, ref=None, files={}): - if ref is not None: - # given q['ref'] and q['files'] replaces references to files by a - # GET to /file?ref=???;name=??? - for k in files: - text = text.replace(k, '/file?ref={};name={}'.format(ref, k)) - return markdown.markdown(text, extensions=[ - 'markdown.extensions.tables', - 'markdown.extensions.fenced_code', - 'markdown.extensions.codehilite', - 'markdown.extensions.def_list', - 'markdown.extensions.sane_lists' - ]) - -def md_to_html_review(text, q): - for k,f in q['files'].items(): - text = text.replace(k, '/absfile?name={}'.format(q['files'][k])) - return markdown.markdown(text, extensions=[ - 'markdown.extensions.tables', - 'markdown.extensions.fenced_code', - 'markdown.extensions.codehilite', - 'markdown.extensions.def_list', - 'markdown.extensions.sane_lists' - ]) + + +# markdown helper +# returns a function md() that renders markdown with extensions +# this function is passed to templates for rendering +def md(text): + return markdown.markdown(text, + extensions=[ + 'markdown.extensions.tables', + 'markdown.extensions.fenced_code', + 'markdown.extensions.codehilite', + 'markdown.extensions.def_list', + 'markdown.extensions.sane_lists' + ]) + + + + + + + + + + + + + + + + + + + +# def md_to_html(text, ref=None, files={}): +# if ref is not None: +# # given q['ref'] and q['files'] replaces references to files by a +# # GET to /file?ref=???;name=??? +# for k in files: +# text = text.replace(k, '/file?ref={};name={}'.format(ref, k)) +# return markdown.markdown(text, extensions=[ +# 'markdown.extensions.tables', +# 'markdown.extensions.fenced_code', +# 'markdown.extensions.codehilite', +# 'markdown.extensions.def_list', +# 'markdown.extensions.sane_lists' +# ]) + +# def md_to_html_review(text, q): +# for k,f in q['files'].items(): +# text = text.replace(k, '/absfile?name={}'.format(q['files'][k])) +# return markdown.markdown(text, extensions=[ +# 'markdown.extensions.tables', +# 'markdown.extensions.fenced_code', +# 'markdown.extensions.codehilite', +# 'markdown.extensions.def_list', +# 'markdown.extensions.sane_lists' +# ]) -- libgit2 0.21.2