from flask import Flask, request, g, session import flask.logging import flask.wrappers from flask_bootstrap import Bootstrap from flask_sqlalchemy import SQLAlchemy import locale import logging import os import tempfile from typing import Optional import werkzeug.exceptions import werkzeug.formparser import mo import mo.config as config import mo.db as db import mo.ext.assets import mo.jobs import mo.rights import mo.users import mo.util # Ohýbáme Flask, aby uploadované soubory ukládal do adresáře podle našeho přání, # aby se pak daly zařadit mezi datové soubory prostým hardlinkováním. Za tímto účelem # subclassujeme Request, aby použil subclassovaný FormDataParser, který použije naši # stream factory místo defaultní. def mo_stream_factory(total_content_length, filename, content_type, content_length=None): return tempfile.NamedTemporaryFile(dir=mo.util.data_dir('tmp'), prefix='upload-') class FormDataParser(werkzeug.formparser.FormDataParser): def __init__(self, stream_factory=None, charset='utf-8', errors='replace', max_form_memory_size=None, max_content_length=None, cls=None, silent=True): super().__init__(mo_stream_factory, charset, errors, max_form_memory_size, max_content_length, cls, silent) class Request(flask.wrappers.Request): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.form_data_parser_class = FormDataParser # Můžeme zvýšit maximální velikost souboru pro dávkové uploady custom_max_content_length: Optional[int] = None # Původně atribut ve werkzeug.BaseRequest, předefinován na property ve flask.Request @property def max_content_length(self): return self.custom_max_content_length or mo.config.MAX_CONTENT_LENGTH # Flask interpretuje relativní cesty všelijak, tak mu vyrobíme absolutní mo.config.DATA_DIR = os.path.abspath(mo.config.DATA_DIR) static_dir = os.path.abspath('static') # Aplikační objekt app = Flask(__name__, static_folder=static_dir) app.config.from_object(config) app.request_class = Request db.flask_db = SQLAlchemy(app, metadata=db.metadata) Bootstrap(app) # make bootstrap libs accessible for the app # Budeme používat české locale locale.setlocale(locale.LC_COLLATE, 'cs_CZ.UTF-8') # Incializace logování def setup_logging(): # Logování v Pythonu je vymyšlené hezky, ale bohužel spousta modulů má tendenci # konfigurovat si u svých loggerů náhodné handlery. Pokud chceme, aby bylo logování # aspoň trochu jednotné, musíme jim to trochu předrátovat. # Nastavíme root logger, aby logoval po našem do flaskového logu log_handler = flask.logging.default_handler log_formatter = logging.Formatter(fmt='%(asctime)-15s.%(msecs)03d %(levelname)-5.5s [%(process)s] (%(name)s) %(message)s', datefmt='%Y-%m-%d %H:%M:%S') log_handler.setFormatter(log_formatter) root_logger = logging.getLogger() root_logger.addHandler(log_handler) # app.logger se jmenuje "mo.web" a propaguje se do root loggeru, # takže nechceme, aby měl svůj handler. app.logger.removeHandler(log_handler) app.logger.setLevel(logging.DEBUG) # sqlalchemy má logger, ke kterému si při prvním vypsání hlášky přidá handler, # není-li tam zatím žádný. Tak přidáme dummy handler. Vše se propaguje do root loggeru. sqla_logger = logging.getLogger('sqlalchemy.engine.base.Engine') sqla_logger.addHandler(logging.NullHandler()) # werkzeug má vlastní handler s vlastním formátem hlášek, který už obsahuje timestamp, # takže vypneme propagování wz_logger = logging.getLogger('werkzeug') wz_logger.propagate = False setup_logging() # Incializace assetového mechanismu mo.ext.assets.Assets(app, url_prefix='/assets', asset_dir=static_dir) app.assets.add_assets([ 'bootstrap.min.css', 'mo.css', 'js/news-reloader.js', ]) # Inicializace požadavků a nucená autorizace class NeedLoginError(werkzeug.exceptions.Forbidden): description = 'Need to log in' def need_login(): if not g.user: raise NeedLoginError() def init_request(): path = request.path if path.startswith('/static/') or path.startswith('/assets/'): # Pro statické soubory v development nasazení nepotřebujeme nastavovat # nic dalšího (v ostrém nasazení je servíruje uwsgi) return if 'uid' in session: user = mo.users.user_by_uid(session['uid']) if not user: # Uživatel mezitím přestal existovat app.logger.error('Zrušena session pro neexistujícího uživatele uid=%s', session['uid']) return mo.web.auth.logout() else: user = None g.user = user if have_uwsgi: uwsgi.set_logvar('osmo_uid', str(user.user_id) if user else '-') mo.now = mo.util.get_now() g.now = mo.now # for templates mo.util.current_log_user = user # K některým podstromům je nutné mít speciální oprávnění # XXX: Když celá aplikace běží v adresáři, request.path je relativní ke kořeni aplikace, ne celého webu if path.startswith('/org/'): if not user: raise NeedLoginError() if not (user.is_org or user.is_admin): raise werkzeug.exceptions.Forbidden() g.gatekeeper = mo.rights.Gatekeeper(user) elif path.startswith('/user/'): if not user: raise NeedLoginError() app.before_request(init_request) ### UWSGI glue ### try: import uwsgi from uwsgidecorators import timer, signal # Čas od času se probudíme a projdeme joby pro případ, že by se ztratil signál. # Také při tom expirujeme zastaralé joby. @timer(config.JOB_GC_PERIOD, target='mule') def mule_timer(signum): # app.logger.debug('Mule: Timer tick') with app.app_context(): mo.now = mo.util.get_now() mo.jobs.process_jobs() # Obykle při vložení jobu dostaneme signál. @signal(42, target='mule') def mule_signal(signum): app.logger.debug('Mule: Přijat signál') with app.app_context(): mo.now = mo.util.get_now() mo.jobs.process_jobs() def wake_up_mule(): app.logger.debug('Mule: Posíláme signál') uwsgi.signal(42) have_uwsgi = True mo.jobs.send_notify = wake_up_mule except ImportError: app.logger.warn('Nenalezeno UWSGI, takže automatické notifikace nepoběží.') have_uwsgi = False # Většina webu je v samostatných modulech import mo.web.auth import mo.web.jinja import mo.web.menu import mo.web.misc import mo.web.org import mo.web.org_contest import mo.web.org_jobs import mo.web.org_log import mo.web.org_place import mo.web.org_round import mo.web.org_score import mo.web.org_users import mo.web.user