changeset 935:7ecb946bfafd

admin: Lots of fixes for running the admin panel in a WSGI server. - Use new source APIs in the dashboard to open WIP files. - Fixed broken/outdated code in some views. - Fixed cases when Flask is not running at the root URL by using the `SCRIPT_NAME` variable somewhat more properly.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 04 Oct 2017 09:15:16 -0700
parents 98430e7143d2
children abc52a6262a1
files piecrust/admin/blueprint.py piecrust/admin/settings.py piecrust/admin/siteinfo.py piecrust/admin/views/__init__.py piecrust/admin/views/create.py piecrust/admin/views/dashboard.py piecrust/admin/views/edit.py piecrust/admin/views/preview.py piecrust/admin/views/sources.py piecrust/admin/web.py piecrust/serving/wrappers.py piecrust/wsgiutil/__init__.py
diffstat 12 files changed, 183 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/admin/blueprint.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/blueprint.py	Wed Oct 04 09:15:16 2017 -0700
@@ -18,22 +18,24 @@
 
 
 def load_user(user_id):
-    admin_id = g.config.get('security/username')
+    admin_id = current_app.config.get('USERNAME')
     if admin_id == user_id:
-        admin_pwd = g.config.get('security/password')
+        admin_pwd = current_app.config.get('PASSWORD')
         return User(admin_id, admin_pwd)
     return None
 
 
-login_manager = LoginManager()
-login_manager.login_view = 'FoodTruck.login'
-login_manager.user_loader(load_user)
+def record_login_manager(state):
+    login_manager = LoginManager()
+    login_manager.login_view = 'FoodTruck.login'
+    login_manager.user_loader(load_user)
 
-
-def record_login_manager(state):
     if state.app.config['SECRET_KEY'] == 'temp-key':
         def _handler():
-            raise Exception("No secret key has been set!")
+            from flask import render_template
+            return render_template(
+                'error.html',
+                error="No secret key has been set!")
 
         logger.debug("No secret key found, disabling website login.")
         login_manager.unauthorized_handler(_handler)
@@ -91,9 +93,8 @@
 @foodtruck_bp.before_request
 def _setup_foodtruck_globals():
     def _get_site():
-        root_dir = current_app.config['FOODTRUCK_ROOT']
-        url_prefix = current_app.config['FOODTRUCK_URL_PREFIX']
-        return SiteInfo(root_dir, url_prefix, debug=current_app.debug)
+        root_dir = current_app.config['FOODTRUCK_ROOT_DIR']
+        return SiteInfo(root_dir, debug=current_app.debug)
 
     g.site = LazySomething(_get_site)
 
@@ -131,5 +132,3 @@
 import piecrust.admin.views.preview  # NOQA
 import piecrust.admin.views.publish  # NOQA
 import piecrust.admin.views.sources  # NOQA
-
-
--- a/piecrust/admin/settings.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/settings.py	Wed Oct 04 09:15:16 2017 -0700
@@ -1,2 +1,1 @@
-FOODTRUCK_URL_PREFIX = '/pc-admin'
-
+FOODTRUCK_ROOT_URL = ''
--- a/piecrust/admin/siteinfo.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/siteinfo.py	Wed Oct 04 09:15:16 2017 -0700
@@ -5,6 +5,7 @@
 import logging
 import threading
 import subprocess
+from flask import request
 from piecrust.app import PieCrustFactory
 
 
@@ -20,15 +21,21 @@
 
 
 class SiteInfo:
-    def __init__(self, root_dir, url_prefix, *, debug=False):
+    def __init__(self, root_dir, *, debug=False):
         self.root_dir = root_dir
-        self.url_prefix = url_prefix
         self.debug = debug
         self._piecrust_factory = None
         self._piecrust_app = None
         self._scm = None
 
     @property
+    def url_prefix(self):
+        return request.script_root
+
+    def make_url(self, rel_url):
+        return self.url_prefix + rel_url
+
+    @property
     def piecrust_factory(self):
         if self._piecrust_factory is None:
             self._piecrust_factory = PieCrustFactory(
@@ -36,10 +43,10 @@
                 cache_key='admin',
                 debug=self.debug,
                 config_values=[
-                    ('site/root', '%s/preview/' % self.url_prefix),
-                    ('site/asset_url_format',
-                     self.url_prefix + '/preview/_asset/%path%')
-                ])
+                    ('site/root', self.make_url('/preview/')),
+                    ('site/asset_url_format', self.make_url(
+                        '/preview/_asset/%path%'))]
+            )
         return self._piecrust_factory
 
     @property
--- a/piecrust/admin/views/__init__.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/views/__init__.py	Wed Oct 04 09:15:16 2017 -0700
@@ -1,4 +1,4 @@
-from flask import render_template, current_app
+from flask import current_app, render_template, request
 from flask.views import View
 from .menu import get_menu_context
 
@@ -31,4 +31,7 @@
 def with_base_data(context=None):
     if context is None:
         context = {}
-    context['base_url'] = current_app.config['FOODTRUCK_URL_PREFIX']
+
+    script_root = request.script_root or ''
+    root_url = current_app.config.get('FOODTRUCK_ROOT_URL') or ''
+    context['base_url'] = script_root + root_url
--- a/piecrust/admin/views/create.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/views/create.py	Wed Oct 04 09:15:16 2017 -0700
@@ -4,6 +4,7 @@
 from flask.ext.login import login_required
 from piecrust.page import Page
 from piecrust.sources.interfaces import IInteractiveSource
+from piecrust.uriutil import split_uri
 from ..blueprint import foodtruck_bp
 from ..views import with_menu_context
 
@@ -72,5 +73,6 @@
     page = Page(source, content_item)
     uri = page.getUri()
     logger.debug("Redirecting to: %s" % uri)
-    return redirect(url_for('.edit_page', uri=uri))
+    _, rel_url = split_uri(page.app, uri)
+    return redirect(url_for('.edit_page', url=rel_url))
 
--- a/piecrust/admin/views/dashboard.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/views/dashboard.py	Wed Oct 04 09:15:16 2017 -0700
@@ -10,7 +10,7 @@
 from piecrust.uriutil import split_uri
 from ..textutil import text_preview
 from ..blueprint import foodtruck_bp, load_user
-from ..views import with_menu_context
+from ..views import with_menu_context, with_base_data
 
 
 logger = logging.getLogger(__name__)
@@ -87,7 +87,8 @@
     if source is None:
         return None
 
-    content_item = source.findContentFromPath(path)
+    full_path = os.path.join(pcapp.root_dir, path)
+    content_item = source.findContentFromPath(full_path)
     if content_item is None:
         return None
 
@@ -103,7 +104,7 @@
     return {
         'title': page.config.get('title'),
         'slug': slug,
-        'url': url_for('.edit_page', uri=slug),
+        'url': url_for('.edit_page', url=slug),
         'text': extract
     }
 
@@ -126,6 +127,7 @@
             "User '%s' doesn't exist or password is incorrect." %
             username)
 
+    with_base_data(data)
     return render_template('login.html', **data)
 
 
--- a/piecrust/admin/views/edit.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/views/edit.py	Wed Oct 04 09:15:16 2017 -0700
@@ -13,85 +13,63 @@
 logger = logging.getLogger(__name__)
 
 
-@foodtruck_bp.route('/edit/', defaults={'uri': ''}, methods=['GET', 'POST'])
-@foodtruck_bp.route('/edit/<path:uri>', methods=['GET', 'POST'])
+@foodtruck_bp.route('/edit/', defaults={'slug': ''}, methods=['GET', 'POST'])
+@foodtruck_bp.route('/edit/<path:slug>', methods=['GET', 'POST'])
 @login_required
-def edit_page(uri):
+def edit_page(slug):
     site = g.site
-    pcapp = site.piecrust_app
-    rp = get_requested_page(pcapp, '%s/preview/%s' % (site.url_prefix, uri))
-    page = rp.page
+    site_app = site.piecrust_app
+    rp = get_requested_page(site_app,
+                            '/site/%s/%s' % (g.sites.current_site, slug))
+    page = rp.qualified_page
     if page is None:
         abort(404)
 
     if request.method == 'POST':
-        return _submit_page_form(page, uri)
+        page_text = request.form['page_text']
+        if request.form['is_dos_nl'] == '0':
+            page_text = page_text.replace('\r\n', '\n')
+
+        if 'do_save' in request.form or 'do_save_and_commit' in request.form:
+            logger.debug("Writing page: %s" % page.path)
+            with open(page.path, 'w', encoding='utf8', newline='') as fp:
+                fp.write(page_text)
+            flash("%s was saved." % os.path.relpath(
+                page.path, site_app.root_dir))
 
-    return _edit_page_form(page, uri)
+        if 'do_save_and_commit' in request.form:
+            message = request.form.get('commit_msg')
+            if not message:
+                message = "Edit %s" % os.path.relpath(
+                    page.path, site_app.root_dir)
+            if site.scm:
+                commit_paths = [page.path]
+                assets_dir = os.path.splitext(page.path)[0] + '-assets'
+                if os.path.isdir(assets_dir):
+                    commit_paths += list(os.listdir(assets_dir))
+                site.scm.commit(commit_paths, message)
+
+        if 'do_save' in request.form or 'do_save_and_commit' in request.form:
+            return _edit_page_form(page, slug, site.name)
+
+        abort(400)
+
+    return _edit_page_form(page, slug, site.name)
 
 
-def _edit_page_form(page, uri):
-    data = {}
-    data['is_new_page'] = False
-    data['url_postback'] = url_for('.edit_page', uri=uri)
-    data['url_upload_asset'] = url_for('.upload_page_asset', uri=uri)
-    data['url_preview'] = page.getUri()
-    data['url_cancel'] = url_for(
-        '.list_source', source_name=page.source.name)
-
-    with page.source.openItem(page.content_item, 'r') as fp:
-        data['page_text'] = fp.read()
-    data['is_dos_nl'] = "1" if '\r\n' in data['page_text'] else "0"
-
-    assetor = Assetor(page)
-    assets_data = []
-    for n in assetor._getAssetNames():
-        assets_data.append({'name': n, 'url': assetor[n]})
-    data['assets'] = assets_data
-
-    data['has_scm'] = (g.site.scm is not None)
-
-    with_menu_context(data)
-    return render_template('edit_page.html', **data)
-
-
-def _submit_page_form(page, uri):
-    page_text = request.form['page_text']
-    if request.form['is_dos_nl'] == '0':
-        page_text = page_text.replace('\r\n', '\n')
-
-    if 'do_save' in request.form or 'do_save_and_commit' in request.form:
-        logger.debug("Writing page: %s" % page.content_spec)
-        with page.source.openItem(page.content_item, 'w') as fp:
-            fp.write(page_text)
-        flash("%s was saved." % page.content_spec)
-
-    scm = g.site.scm
-    if 'do_save_and_commit' in request.form and scm is not None:
-        message = request.form.get('commit_msg')
-        if not message:
-            message = "Edit %s" % page.content_spec
-        scm.commit([page.content_spec], message)
-
-    if 'do_save' in request.form or 'do_save_and_commit' in request.form:
-        return _edit_page_form(page, uri)
-
-    abort(400)
-
-
-@foodtruck_bp.route('/upload/<path:uri>', methods=['POST'])
-def upload_page_asset(uri):
+@foodtruck_bp.route('/upload/<path:slug>', methods=['POST'])
+def upload_page_asset(slug):
     if 'ft-asset-file' not in request.files:
-        return redirect(url_for('.edit_page', uri=uri))
+        return redirect(url_for('.edit_page', slug=slug))
 
     asset_file = request.files['ft-asset-file']
     if asset_file.filename == '':
-        return redirect(url_for('.edit_page', uri=uri))
+        return redirect(url_for('.edit_page', slug=slug))
 
     site = g.site
-    pcapp = site.piecrust_app
-    rp = get_requested_page(pcapp,
-                            '/site/%s/%s' % (g.sites.current_site, uri))
+    site_app = site.piecrust_app
+    rp = get_requested_page(site_app,
+                            '/site/%s/%s' % (g.sites.current_site, slug))
     page = rp.qualified_page
     if page is None:
         abort(404)
@@ -109,4 +87,29 @@
     asset_path = os.path.join(dirname, filename)
     logger.info("Uploading file to: %s" % asset_path)
     asset_file.save(asset_path)
-    return redirect(url_for('.edit_page', uri=uri))
+    return redirect(url_for('.edit_page', slug=slug))
+
+
+def _edit_page_form(page, slug, sitename):
+    data = {}
+    data['is_new_page'] = False
+    data['url_postback'] = url_for('.edit_page', slug=slug)
+    data['url_upload_asset'] = url_for('.upload_page_asset', slug=slug)
+    data['url_preview'] = page.getUri()
+    data['url_cancel'] = url_for(
+        '.list_source', source_name=page.source.name)
+    with open(page.path, 'r', encoding='utf8', newline='') as fp:
+        data['page_text'] = fp.read()
+    data['is_dos_nl'] = "1" if '\r\n' in data['page_text'] else "0"
+
+    page.app.env.base_asset_url_format = \
+        page.app.config.get('site/root') + '_asset/%path%'
+    assetor = Assetor(page, 'blah')
+    assets_data = []
+    for n in assetor.allNames():
+        assets_data.append({'name': n, 'url': assetor[n]})
+    data['assets'] = assets_data
+
+    with_menu_context(data)
+    return render_template('edit_page.html', **data)
+
--- a/piecrust/admin/views/preview.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/views/preview.py	Wed Oct 04 09:15:16 2017 -0700
@@ -1,4 +1,4 @@
-from flask import current_app, g, make_response
+from flask import g, make_response
 from flask.ext.login import login_required
 from piecrust.serving.server import PieCrustServer
 from ..blueprint import foodtruck_bp
@@ -14,8 +14,6 @@
 @login_required
 def preview_page(url):
     pcappfac = g.site.piecrust_factory
-    url_prefix = current_app.config['FOODTRUCK_URL_PREFIX']
-    server = PieCrustServer(pcappfac,
-                            root_url='%s/preview/' % url_prefix)
+    server = PieCrustServer(pcappfac, root_url=g.site.make_url('/preview/'))
     return make_response(server)
 
--- a/piecrust/admin/views/sources.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/views/sources.py	Wed Oct 04 09:15:16 2017 -0700
@@ -27,7 +27,7 @@
             'tags': p.get('tags', []),
             'category': p.get('category'),
             'source': source_name,
-            'url': url_for('.edit_page', uri=p['slug'])
+            'url': url_for('.edit_page', url=p['rel_url'])
         }
         data['pages'].append(page_data)
 
--- a/piecrust/admin/web.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/admin/web.py	Wed Oct 04 09:15:16 2017 -0700
@@ -7,7 +7,7 @@
 logger = logging.getLogger(__name__)
 
 
-def create_foodtruck_app(extra_settings=None):
+def create_foodtruck_app(extra_settings=None, url_prefix=None):
     from .blueprint import foodtruck_bp
 
     app = Flask(__name__.split('.')[0], static_folder=None)
@@ -15,17 +15,12 @@
     if extra_settings:
         app.config.update(extra_settings)
 
-    root_dir = app.config.setdefault('FOODTRUCK_ROOT', os.getcwd())
+    root_dir = app.config.setdefault('FOODTRUCK_ROOT_DIR', os.getcwd())
 
     app.config.from_pyfile(os.path.join(root_dir, 'admin_app.cfg'),
                            silent=True)
     app.config.from_envvar('FOODTRUCK_SETTINGS', silent=True)
 
-    # Add a special route for the `.well-known` directory.
-    app.wsgi_app = SharedDataMiddleware(
-        app.wsgi_app,
-        {'/.well-known': os.path.join(root_dir, '.well-known')})
-
     # Setup logging/error handling.
     if app.config['DEBUG']:
         l = logging.getLogger()
@@ -37,8 +32,32 @@
         app.config['SECRET_KEY'] = 'temp-key'
 
     # Register extensions and blueprints.
-    bp_prefix = app.config['FOODTRUCK_URL_PREFIX']
-    app.register_blueprint(foodtruck_bp, url_prefix=bp_prefix)
+    app.register_blueprint(foodtruck_bp, url_prefix=url_prefix)
+
+    # ---------------
+    # Debugging stuff
+    @app.errorhandler(404)
+    def page_not_found(e):
+        from flask import request, url_for
+        output = []
+        for rule in app.url_map.iter_rules():
+            options = {}
+            for arg in rule.arguments:
+                options[arg] = "[{0}]".format(arg)
+                methods = ','.join(rule.methods)
+                try:
+                    url = url_for(rule.endpoint, **options)
+                except:
+                    url = '???'
+                line = ("{:50s} {:20s} {}".format(rule.endpoint, methods, url))
+                output.append(line)
+
+        resp = request.path + '<br/>\n'
+        resp += str(request.environ) + '<br/>\n'
+        resp += str(app.config['FOODTRUCK_ROOT_URL']) + '<br/>\n'
+        resp += '<br/>\n'.join(sorted(output))
+        return resp, 404
+    # ---------------
 
     logger.debug("Created FoodTruck app with admin root: %s" % root_dir)
 
--- a/piecrust/serving/wrappers.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/serving/wrappers.py	Wed Oct 04 09:15:16 2017 -0700
@@ -142,10 +142,27 @@
     app_wrapper.run()
 
 
+def get_piecrust_server(root_dir, *,
+                        debug=False,
+                        cache_key=None,
+                        serve_site=True,
+                        serve_admin=False,
+                        is_cmdline_mode=False):
+    from piecrust.app import PieCrustFactory
+    appfactory = PieCrustFactory(root_dir,
+                                 debug=debug,
+                                 cache_key=cache_key)
+    return _get_piecrust_server(appfactory,
+                                serve_site=serve_site,
+                                serve_admin=serve_admin,
+                                is_cmdline_mode=is_cmdline_mode)
+
+
 def _get_piecrust_server(appfactory, *,
                          serve_site=True,
                          serve_admin=False,
                          is_cmdline_mode=False,
+                         admin_root_url=None,
                          run_sse_check=None):
     app = None
 
@@ -164,11 +181,10 @@
     if serve_admin:
         from piecrust.admin.web import create_foodtruck_app
 
-        admin_root_url = '/pc-admin'
         es = {
             'FOODTRUCK_CMDLINE_MODE': is_cmdline_mode,
-            'FOODTRUCK_ROOT': appfactory.root_dir,
-            'FOODTRUCK_URL_PREFIX': admin_root_url,
+            'FOODTRUCK_ROOT_DIR': appfactory.root_dir,
+            'FOODTRUCK_ROOT_URL': admin_root_url,
             'DEBUG': appfactory.debug}
         if is_cmdline_mode:
             es.update({
@@ -179,9 +195,11 @@
             # Disable PIN protection with Werkzeug's debugger.
             os.environ['WERKZEUG_DEBUG_PIN'] = 'off'
 
-        admin_app = create_foodtruck_app(es)
-        admin_app.wsgi_app = _PieCrustSiteOrAdminMiddleware(
-            app, admin_app.wsgi_app, admin_root_url)
+        admin_app = create_foodtruck_app(es, url_prefix=admin_root_url)
+        if app is not None:
+            admin_app.wsgi_app = _PieCrustSiteOrAdminMiddleware(
+                app, admin_app.wsgi_app, admin_root_url)
+
         app = admin_app
 
     return app
@@ -203,3 +221,13 @@
         if path_info.startswith(self.admin_root_url):
             return self.admin_app(environ, start_response)
         return self.main_app(environ, start_response)
+
+
+class _PieCrustAdminScriptNamePatcherMiddleware:
+    def __init__(self, admin_app, admin_root_url):
+        self.admin_app = admin_app
+        self.admin_root_url = '/%s' % admin_root_url.strip('/')
+
+    def __call__(self, environ, start_response):
+        environ['SCRIPT_NAME'] = self.admin_root_url
+        return self.admin_app(environ, start_response)
--- a/piecrust/wsgiutil/__init__.py	Wed Oct 04 09:11:58 2017 -0700
+++ b/piecrust/wsgiutil/__init__.py	Wed Oct 04 09:15:16 2017 -0700
@@ -1,5 +1,5 @@
 import logging
-from piecrust.serving.server import WsgiServer
+from piecrust.serving.wrappers import get_piecrust_server
 
 
 def _setup_logging(log_file, log_level, max_log_bytes, log_backup_count):
@@ -13,29 +13,29 @@
 
 def get_app(root_dir, *,
             cache_key='prod',
-            enable_debug_info=False,
+            serve_admin=False,
             log_file=None,
             log_level=logging.INFO,
             log_backup_count=0,
             max_log_bytes=4096):
     _setup_logging(log_file, log_level, max_log_bytes, log_backup_count)
-    app = WsgiServer(root_dir,
-                     cache_key=cache_key,
-                     enable_debug_info=enable_debug_info)
+    app = get_piecrust_server(root_dir,
+                              serve_site=True,
+                              serve_admin=serve_admin,
+                              cache_key=cache_key)
     return app
 
 
 def get_admin_app(root_dir, *,
-                  url_prefix='pc-admin',
+                  cache_key='prod',
                   log_file=None,
                   log_level=logging.INFO,
                   log_backup_count=0,
                   max_log_bytes=4096):
     _setup_logging(log_file, log_level, max_log_bytes, log_backup_count)
-    es = {
-        'FOODTRUCK_ROOT': root_dir,
-        'FOODTRUCK_URL_PREFIX': url_prefix}
-    from piecrust.admin.web import create_foodtruck_app
-    app = create_foodtruck_app(es)
+    app = get_piecrust_server(root_dir,
+                              serve_site=False,
+                              serve_admin=True,
+                              cache_key=cache_key)
     return app