Commit a96cd2c78100a559e52fb5f5850fae498875f77b
1 parent
714897fd
Exists in
master
and in
1 other branch
- added logging system.
- improved documentation. - useful feedback when importing modules fails. - markdown helper md() moved to tools. - added empty certs folder (.gitignore)
Showing
7 changed files
with
189 additions
and
59 deletions
Show diff stats
BUGS.md
| 1 | 1 | BUGS: |
| 2 | 2 | |
| 3 | +- se students.db não existe, rebenta. | |
| 4 | +- textarea tem codigo para preencher o texto, mas ja não é necessário porque pergunta não é reloaded. | |
| 3 | 5 | - questions hardcoded in LearnApp. |
| 4 | 6 | - database hardcoded in LearnApp. |
| 5 | 7 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() |
| 6 | 8 | |
| 7 | 9 | TODO: |
| 8 | 10 | |
| 11 | +- configuração e linha de comando. | |
| 12 | +- logging | |
| 9 | 13 | - como gerar uma sequencia de perguntas? |
| 10 | 14 | - generators not working: bcrypt (ver blog) |
| 11 | 15 | - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. | ... | ... |
README.md
| 1 | +# Get Started | |
| 2 | + | |
| 3 | + | |
| 1 | 4 | ## Requirements |
| 2 | 5 | |
| 3 | -You will need to install `python3.6`, `pip` and `sqlite3`. | |
| 4 | -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`). | |
| 6 | +We will need to install python3.6, pip and sqlite3 python package. | |
| 7 | +This can be done using the system package management, downloaded from [http://www.python.org](), or compiled from sources. | |
| 8 | + | |
| 9 | +- Installing from the system package management: | |
| 10 | + - OSX: `port install python36` | |
| 11 | + - FreeBSD: `pkg install python36 py36-sqlite3` | |
| 12 | + - Linux: `apt-get install ???` | |
| 13 | +- Installing from sources: | |
| 14 | + - Download from [http://www.python.org]() | |
| 15 | + - `unxz Python-3.6.tar.xz` | |
| 16 | + - `tar xvf Python-3.6.tar` | |
| 17 | + - `cd Python-3.6` | |
| 18 | + - `./configure --prefix=$HOME/.local/bin` | |
| 19 | + - `make && make install` | |
| 20 | + | |
| 21 | +To install pip (if not yet installed): | |
| 22 | + | |
| 23 | + python36 -m ensurepip --user | |
| 5 | 24 | |
| 6 | -Install some python packages locally on the user area: | |
| 25 | +This will install pip in your account under `~/.local/bin`. | |
| 26 | +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). | |
| 27 | + | |
| 28 | +Install additional python packages locally on the user area: | |
| 7 | 29 | |
| 8 | 30 | pip install --user tornado sqlalchemy pyyaml pygments markdown bcrypt |
| 9 | 31 | |
| ... | ... | @@ -12,6 +34,12 @@ These are usually installed under |
| 12 | 34 | - OSX: `~/Library/python/3.6/lib/python/site-packages/` |
| 13 | 35 | - Linux/FreeBSD: `~/.local/lib/python3.6/site-packages/` |
| 14 | 36 | |
| 37 | +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 | |
| 38 | + | |
| 39 | + [global] | |
| 40 | + user = yes | |
| 41 | + | |
| 42 | + | |
| 15 | 43 | ## Installation |
| 16 | 44 | |
| 17 | 45 | Replace USER by your bitbucket username: |
| ... | ... | @@ -30,7 +58,7 @@ First we need to create a database: |
| 30 | 58 | ./initdb.py # initialize with a single user `0` and empty password |
| 31 | 59 | ./initdb.py --help # for the available options |
| 32 | 60 | |
| 33 | -We also need certificates for https. We can generate selfsigned certificates using openssl: | |
| 61 | +We also need certificates for https. Generate selfsigned certificates using openssl: | |
| 34 | 62 | |
| 35 | 63 | mkdir certs |
| 36 | 64 | cd certs | ... | ... |
app.py
| ... | ... | @@ -2,16 +2,24 @@ |
| 2 | 2 | import random |
| 3 | 3 | from contextlib import contextmanager # `with` statement in db sessions |
| 4 | 4 | from datetime import datetime |
| 5 | +import logging | |
| 5 | 6 | |
| 6 | -# libs | |
| 7 | -import bcrypt | |
| 8 | -from sqlalchemy import create_engine | |
| 9 | -from sqlalchemy.orm import sessionmaker #, scoped_session | |
| 7 | +# user installed libraries | |
| 8 | +try: | |
| 9 | + import bcrypt | |
| 10 | + from sqlalchemy import create_engine | |
| 11 | + from sqlalchemy.orm import sessionmaker | |
| 12 | +except ImportError: | |
| 13 | + logger.critical('Python package missing. See README.md for instructions.') | |
| 14 | + sys.exit(1) | |
| 10 | 15 | |
| 11 | 16 | # this project |
| 12 | 17 | import questions |
| 13 | 18 | from models import Student, Answer |
| 14 | 19 | |
| 20 | +# setup logger for this module | |
| 21 | +logger = logging.getLogger(__name__) | |
| 22 | + | |
| 15 | 23 | # ============================================================================ |
| 16 | 24 | # LearnApp - application logic |
| 17 | 25 | # ============================================================================ |
| ... | ... | @@ -30,21 +38,23 @@ class LearnApp(object): |
| 30 | 38 | with self.db_session() as s: |
| 31 | 39 | n = s.query(Student).count() # filter(Student.id != '0'). |
| 32 | 40 | except Exception as e: |
| 33 | - print('Database not usable.') | |
| 41 | + logger.critical('Database not usable.') | |
| 34 | 42 | raise e |
| 35 | 43 | else: |
| 36 | - print('Database has {} students registered.'.format(n)) | |
| 44 | + logger.info(f'Database has {n} students registered.') | |
| 37 | 45 | |
| 38 | 46 | # ------------------------------------------------------------------------ |
| 39 | 47 | def login(self, uid, try_pw): |
| 40 | 48 | with self.db_session() as s: |
| 41 | 49 | student = s.query(Student).filter(Student.id == uid).one_or_none() |
| 42 | 50 | |
| 43 | - if student is None or student in self.online: # FIXME | |
| 51 | + if student is None: | |
| 52 | + logger.info(f'User "{uid}" does not exist.') | |
| 44 | 53 | return False # student does not exist or already loggeg in |
| 45 | 54 | |
| 46 | 55 | hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) |
| 47 | 56 | if hashedtry != student.password: |
| 57 | + logger.info(f'User "{uid}" wrong password.') | |
| 48 | 58 | return False # wrong password |
| 49 | 59 | |
| 50 | 60 | # success | ... | ... |
| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | + | |
| 2 | +version: 1 | |
| 3 | + | |
| 4 | +formatters: | |
| 5 | + void: | |
| 6 | + format: '' | |
| 7 | + standard: | |
| 8 | + format: '%(asctime)s | %(levelname)-8s | %(name)-14s | %(message)s' | |
| 9 | + | |
| 10 | +handlers: | |
| 11 | + default: | |
| 12 | + level: 'INFO' | |
| 13 | + class: 'logging.StreamHandler' | |
| 14 | + formatter: 'standard' | |
| 15 | + stream: 'ext://sys.stdout' | |
| 16 | + | |
| 17 | +loggers: | |
| 18 | + '': | |
| 19 | + handlers: ['default'] | |
| 20 | + level: 'INFO' | |
| 21 | + | |
| 22 | + 'app': | |
| 23 | + handlers: ['default'] | |
| 24 | + level: 'INFO' | |
| 25 | + propagate: False | |
| 26 | + | |
| 27 | + 'questions': | |
| 28 | + handlers: ['default'] | |
| 29 | + level: 'INFO' | |
| 30 | + propagate: False | |
| 31 | + | |
| 32 | + 'tools': | |
| 33 | + handlers: ['default'] | |
| 34 | + level: 'INFO' | |
| 35 | + propagate: False | |
| 36 | + | ... | ... |
serve.py
| ... | ... | @@ -2,34 +2,30 @@ |
| 2 | 2 | |
| 3 | 3 | # python standard library |
| 4 | 4 | import os |
| 5 | +import sys | |
| 5 | 6 | import json |
| 6 | 7 | import base64 |
| 7 | 8 | import uuid |
| 8 | - | |
| 9 | -# installed libraries | |
| 10 | -import markdown | |
| 11 | -import tornado.ioloop | |
| 12 | -import tornado.web | |
| 13 | -import tornado.httpserver | |
| 14 | -from tornado import template, gen | |
| 15 | 9 | import concurrent.futures |
| 10 | +import logging.config | |
| 11 | + | |
| 12 | +# user installed libraries | |
| 13 | +try: | |
| 14 | + import markdown | |
| 15 | + import tornado.ioloop | |
| 16 | + import tornado.web | |
| 17 | + import tornado.httpserver | |
| 18 | + from tornado import template, gen | |
| 19 | +except ImportError: | |
| 20 | + print('Some python packages are missing. See README.md for instructions.') | |
| 21 | + sys.exit(1) | |
| 16 | 22 | |
| 17 | 23 | # this project |
| 18 | 24 | from app import LearnApp |
| 19 | - | |
| 20 | -# markdown helper | |
| 21 | -def md(text): | |
| 22 | - return markdown.markdown(text, | |
| 23 | - extensions=[ | |
| 24 | - 'markdown.extensions.tables', | |
| 25 | - 'markdown.extensions.fenced_code', | |
| 26 | - 'markdown.extensions.codehilite', | |
| 27 | - 'markdown.extensions.def_list', | |
| 28 | - 'markdown.extensions.sane_lists' | |
| 29 | - ]) | |
| 25 | +from tools import load_yaml, md | |
| 30 | 26 | |
| 31 | 27 | # A thread pool to be used for password hashing with bcrypt. FIXME and other things? |
| 32 | -executor = concurrent.futures.ThreadPoolExecutor(2) | |
| 28 | +# executor = concurrent.futures.ThreadPoolExecutor(2) | |
| 33 | 29 | |
| 34 | 30 | |
| 35 | 31 | # ============================================================================ |
| ... | ... | @@ -89,11 +85,11 @@ class LoginHandler(BaseHandler): |
| 89 | 85 | # print(f'login.post: user={uid}, pw={pw}') |
| 90 | 86 | |
| 91 | 87 | if self.learn.login(uid, pw): |
| 92 | - print('login ok') | |
| 88 | + logging.info(f'User "{uid}" login ok.') | |
| 93 | 89 | self.set_secure_cookie("user", str(uid), expires_days=30) |
| 94 | 90 | self.redirect(self.get_argument("next", "/")) |
| 95 | 91 | else: |
| 96 | - print('login failed') | |
| 92 | + logging.info(f'User "{uid}" login failed.') | |
| 97 | 93 | self.render("login.html", error='Número ou senha incorrectos') |
| 98 | 94 | |
| 99 | 95 | # ---------------------------------------------------------------------------- |
| ... | ... | @@ -159,19 +155,40 @@ class QuestionHandler(BaseHandler): |
| 159 | 155 | |
| 160 | 156 | # ---------------------------------------------------------------------------- |
| 161 | 157 | def main(): |
| 162 | - webapp = WebApplication() | |
| 158 | + SERVER_PATH = os.path.dirname(os.path.realpath(__file__)) | |
| 159 | + LOGGER_CONF = os.path.join(SERVER_PATH, 'config/logger.yaml') | |
| 160 | + | |
| 161 | + | |
| 162 | + # --- Setup logging | |
| 163 | + try: | |
| 164 | + logging.config.dictConfig(load_yaml(LOGGER_CONF)) | |
| 165 | + except: # FIXME should this be done in a different way? | |
| 166 | + print('An error ocurred while setting up the logging system.') | |
| 167 | + print('Common causes:\n - inexistent directory "logs"?\n - write permission to "logs" directory?') | |
| 168 | + sys.exit(1) | |
| 169 | + | |
| 170 | + # --- start application | |
| 171 | + try: | |
| 172 | + webapp = WebApplication() | |
| 173 | + except: | |
| 174 | + logging.critical('Can\'t start application.') | |
| 175 | + sys.exit(1) | |
| 176 | + | |
| 177 | + # --- create webserver | |
| 163 | 178 | http_server = tornado.httpserver.HTTPServer(webapp, ssl_options={ |
| 164 | 179 | "certfile": "certs/cert.pem", |
| 165 | 180 | "keyfile": "certs/key.pem" |
| 166 | 181 | }) |
| 167 | 182 | http_server.listen(8443) |
| 168 | 183 | |
| 184 | + # --- start webserver | |
| 169 | 185 | try: |
| 170 | - print('--- start ---') | |
| 186 | + logging.info('Webserver running...') | |
| 171 | 187 | tornado.ioloop.IOLoop.current().start() |
| 188 | + # running... | |
| 172 | 189 | except KeyboardInterrupt: |
| 173 | 190 | tornado.ioloop.IOLoop.current().stop() |
| 174 | - print('\n--- stop ---') | |
| 191 | + logging.info('Webserver stopped.') | |
| 175 | 192 | |
| 176 | 193 | # ---------------------------------------------------------------------------- |
| 177 | 194 | if __name__ == "__main__": | ... | ... |
tools.py
| ... | ... | @@ -55,27 +55,60 @@ def run_script(script, stdin='', timeout=5): |
| 55 | 55 | else: |
| 56 | 56 | return output |
| 57 | 57 | |
| 58 | -def md_to_html(text, ref=None, files={}): | |
| 59 | - if ref is not None: | |
| 60 | - # given q['ref'] and q['files'] replaces references to files by a | |
| 61 | - # GET to /file?ref=???;name=??? | |
| 62 | - for k in files: | |
| 63 | - text = text.replace(k, '/file?ref={};name={}'.format(ref, k)) | |
| 64 | - return markdown.markdown(text, extensions=[ | |
| 65 | - 'markdown.extensions.tables', | |
| 66 | - 'markdown.extensions.fenced_code', | |
| 67 | - 'markdown.extensions.codehilite', | |
| 68 | - 'markdown.extensions.def_list', | |
| 69 | - 'markdown.extensions.sane_lists' | |
| 70 | - ]) | |
| 71 | - | |
| 72 | -def md_to_html_review(text, q): | |
| 73 | - for k,f in q['files'].items(): | |
| 74 | - text = text.replace(k, '/absfile?name={}'.format(q['files'][k])) | |
| 75 | - return markdown.markdown(text, extensions=[ | |
| 76 | - 'markdown.extensions.tables', | |
| 77 | - 'markdown.extensions.fenced_code', | |
| 78 | - 'markdown.extensions.codehilite', | |
| 79 | - 'markdown.extensions.def_list', | |
| 80 | - 'markdown.extensions.sane_lists' | |
| 81 | - ]) | |
| 58 | + | |
| 59 | + | |
| 60 | +# markdown helper | |
| 61 | +# returns a function md() that renders markdown with extensions | |
| 62 | +# this function is passed to templates for rendering | |
| 63 | +def md(text): | |
| 64 | + return markdown.markdown(text, | |
| 65 | + extensions=[ | |
| 66 | + 'markdown.extensions.tables', | |
| 67 | + 'markdown.extensions.fenced_code', | |
| 68 | + 'markdown.extensions.codehilite', | |
| 69 | + 'markdown.extensions.def_list', | |
| 70 | + 'markdown.extensions.sane_lists' | |
| 71 | + ]) | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | +# def md_to_html(text, ref=None, files={}): | |
| 92 | +# if ref is not None: | |
| 93 | +# # given q['ref'] and q['files'] replaces references to files by a | |
| 94 | +# # GET to /file?ref=???;name=??? | |
| 95 | +# for k in files: | |
| 96 | +# text = text.replace(k, '/file?ref={};name={}'.format(ref, k)) | |
| 97 | +# return markdown.markdown(text, extensions=[ | |
| 98 | +# 'markdown.extensions.tables', | |
| 99 | +# 'markdown.extensions.fenced_code', | |
| 100 | +# 'markdown.extensions.codehilite', | |
| 101 | +# 'markdown.extensions.def_list', | |
| 102 | +# 'markdown.extensions.sane_lists' | |
| 103 | +# ]) | |
| 104 | + | |
| 105 | +# def md_to_html_review(text, q): | |
| 106 | +# for k,f in q['files'].items(): | |
| 107 | +# text = text.replace(k, '/absfile?name={}'.format(q['files'][k])) | |
| 108 | +# return markdown.markdown(text, extensions=[ | |
| 109 | +# 'markdown.extensions.tables', | |
| 110 | +# 'markdown.extensions.fenced_code', | |
| 111 | +# 'markdown.extensions.codehilite', | |
| 112 | +# 'markdown.extensions.def_list', | |
| 113 | +# 'markdown.extensions.sane_lists' | |
| 114 | +# ]) | ... | ... |