diff foodtruck/web.py @ 772:3885421c29a3

admin: Make the whole FoodTruck site into a blueprint. This makes it possible to use an app factory, which makes it easier to write unit tests.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 03 Jul 2016 07:54:54 -0700
parents e67da1f7293b
children ba0a6bd5e913
line wrap: on
line diff
--- a/foodtruck/web.py	Sun Jul 03 07:53:17 2016 -0700
+++ b/foodtruck/web.py	Sun Jul 03 07:54:54 2016 -0700
@@ -1,190 +1,70 @@
-import os
 import os.path
-import time
 import logging
-from flask import Flask, g, request, render_template
+from flask import Flask
 from werkzeug import SharedDataMiddleware
-from .configuration import (
-        FoodTruckConfigNotFoundError, get_foodtruck_config)
-from .sites import FoodTruckSites, InvalidSiteError
+from .blueprint import foodtruck_bp, login_manager, bcrypt_ext
 
 
 logger = logging.getLogger(__name__)
 
-app = Flask(__name__)
-app.config.from_object('foodtruck.settings')
-app.config.from_envvar('FOODTRUCK_SETTINGS', silent=True)
-
-admin_root = app.config.get('FOODTRUCK_ROOT', os.getcwd())
-config_path = os.path.join(admin_root, 'app.cfg')
-
-# If we're being run as the `chef admin run` command, from inside a PieCrust
-# website, do a few things differently.
-_procedural_config = None
-
-if (app.config.get('FOODTRUCK_CMDLINE_MODE', False) and
-        os.path.isfile(os.path.join(admin_root, 'config.yml'))):
-    app.secret_key = os.urandom(22)
-    app.config['LOGIN_DISABLED'] = True
-    _procedural_config = {
-            'sites': {
-                'local': admin_root}
-            }
 
-# Add a special route for the `.well-known` directory.
-app.wsgi_app = SharedDataMiddleware(
-        app.wsgi_app,
-        {'/.well-known': os.path.join(admin_root, '.well-known')})
-
-if os.path.isfile(config_path):
-    app.config.from_pyfile(config_path)
+def create_foodtruck_app(extra_settings=None):
+    app = Flask(__name__)
+    app.config.from_object('foodtruck.settings')
+    app.config.from_envvar('FOODTRUCK_SETTINGS', silent=True)
+    if extra_settings:
+        app.config.from_object(extra_settings)
 
-if app.config['DEBUG']:
-    l = logging.getLogger()
-    l.setLevel(logging.DEBUG)
-
-logger.debug("Using FoodTruck admin root: %s" % admin_root)
-
-
-def after_this_request(f):
-    if not hasattr(g, 'after_request_callbacks'):
-        g.after_request_callbacks = []
-    g.after_request_callbacks.append(f)
-    return f
-
+    admin_root = app.config.setdefault('FOODTRUCK_ROOT', os.getcwd())
+    config_path = os.path.join(admin_root, 'app.cfg')
 
-class LazySomething(object):
-    def __init__(self, factory):
-        self._factory = factory
-        self._something = None
-
-    def __getattr__(self, name):
-        if self._something is not None:
-            return getattr(self._something, name)
-
-        self._something = self._factory()
-        return getattr(self._something, name)
-
-
-@app.before_request
-def _setup_foodtruck_globals():
-    def _get_config():
-        return get_foodtruck_config(admin_root, _procedural_config)
+    # If we're being run as the `chef admin run` command, from inside a PieCrust
+    # website, do a few things differently.
+    app.config['FOODTRUCK_PROCEDURAL_CONFIG'] = None
+    if (app.config.get('FOODTRUCK_CMDLINE_MODE', False) and
+            os.path.isfile(os.path.join(admin_root, 'config.yml'))):
+        app.secret_key = os.urandom(22)
+        app.config['LOGIN_DISABLED'] = True
+        app.config['FOODTRUCK_PROCEDURAL_CONFIG'] = {
+                'sites': {
+                    'local': admin_root}
+                }
 
-    def _get_sites():
-        names = g.config.get('sites')
-        if not names or not isinstance(names, dict):
-            raise InvalidSiteError(
-                    "No sites are defined in the configuration file.")
-
-        current = request.cookies.get('foodtruck_site_name')
-        if current is not None and current not in names:
-            current = None
-        if current is None:
-            current = next(iter(names.keys()))
-        s = FoodTruckSites(g.config, current)
-        return s
+    # Add a special route for the `.well-known` directory.
+    app.wsgi_app = SharedDataMiddleware(
+            app.wsgi_app,
+            {'/.well-known': os.path.join(admin_root, '.well-known')})
 
-    def _get_current_site():
-        return g.sites.get()
-
-    g.config = LazySomething(_get_config)
-    g.sites = LazySomething(_get_sites)
-    g.site = LazySomething(_get_current_site)
-
-
-@app.after_request
-def _call_after_request_callbacks(response):
-    for callback in getattr(g, 'after_request_callbacks', ()):
-        callback(response)
-    return response
-
+    if os.path.isfile(config_path):
+        app.config.from_pyfile(config_path)
 
-if not app.config['DEBUG']:
-    logger.debug("Registering exception handlers.")
-
-    @app.errorhandler(FoodTruckConfigNotFoundError)
-    def _on_config_missing(ex):
-        return render_template('install.html')
-
-    @app.errorhandler(InvalidSiteError)
-    def _on_invalid_site(ex):
-        data = {'error': "The was an error with your configuration file: %s" %
-                str(ex)}
-        return render_template('error.html', **data)
-
-
-@app.errorhandler
-def _on_error(ex):
-    logging.exception(ex)
-
-
-_missing_secret_key = False
+    if app.config['DEBUG']:
+        l = logging.getLogger()
+        l.setLevel(logging.DEBUG)
+    else:
+        @app.errorhandler(FoodTruckConfigNotFoundError)
+        def _on_config_missing(ex):
+            return render_template('install.html')
 
-if not app.secret_key:
-    # If there's no secret key, create a temp one but mark the app as not
-    # correctly installed so it shows the installation information page.
-    app.secret_key = 'temp-key'
-    _missing_secret_key = True
-
-
-from flask.ext.login import LoginManager, UserMixin
-
-
-class User(UserMixin):
-    def __init__(self, uid, pwd):
-        self.id = uid
-        self.password = pwd
-
-
-def load_user(user_id):
-    admin_id = g.config.get('security/username')
-    if admin_id == user_id:
-        admin_pwd = g.config.get('security/password')
-        return User(admin_id, admin_pwd)
-    return None
+        @app.errorhandler(InvalidSiteError)
+        def _on_invalid_site(ex):
+            data = {'error': "The was an error with your configuration file: %s" %
+                    str(ex)}
+            return render_template('error.html', **data)
 
-
-login_manager = LoginManager()
-login_manager.init_app(app)
-login_manager.login_view = 'login'
-login_manager.user_loader(load_user)
-
-if _missing_secret_key:
-    def _handler():
-        raise FoodTruckConfigNotFoundError()
-
-    logger.debug("No secret key found, disabling website.")
-    login_manager.unauthorized_handler(_handler)
-    login_manager.login_view = None
-
+    _missing_secret_key = False
+    if not app.secret_key:
+        # If there's no secret key, create a temp one but mark the app as not
+        # correctly installed so it shows the installation information page.
+        app.secret_key = 'temp-key'
+        _missing_secret_key = True
 
-from foodtruck.bcryptfallback import Bcrypt
-if (getattr(Bcrypt, 'is_fallback_bcrypt', None) is True and
-        not app.config.get('FOODTRUCK_CMDLINE_MODE', False)):
-    raise Exception(
-            "You're running FoodTruck outside of `chef`, and will need to "
-            "install Flask-Bcrypt to get more proper security.")
-app.bcrypt = Bcrypt(app)
-
-
-@app.template_filter('iso8601')
-def timestamp_to_iso8601(t):
-    t = time.localtime(t)
-    return time.strftime('%Y-%m-%dT%H:%M:%SZ', t)
+    # Register extensions and blueprints.
+    login_manager.init_app(app)
+    bcrypt_ext.init_app(app)
+    app.register_blueprint(foodtruck_bp)
 
-@app.template_filter('datetime')
-def timestamp_to_datetime(t, fmt=None):
-    fmt = fmt or '%x'
-    t = time.localtime(t)
-    return time.strftime(fmt, t)
-
+    logger.debug("Created FoodTruck app with admin root: %s" % admin_root)
 
-import foodtruck.views.create  # NOQA
-import foodtruck.views.dashboard  # NOQA
-import foodtruck.views.edit  # NOQA
-import foodtruck.views.menu  # NOQA
-import foodtruck.views.preview  # NOQA
-import foodtruck.views.publish  # NOQA
-import foodtruck.views.sources  # NOQA
+    return app