changeset 886:dcdec4b951a1

admin: Get the admin panel working again.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 20 Jun 2017 21:13:08 -0700
parents 13e8b50a2113
children c0cbcd4752f0
files piecrust/admin/blueprint.py piecrust/admin/configuration.py piecrust/admin/main.py piecrust/admin/settings.py piecrust/admin/siteinfo.py piecrust/admin/sites.py piecrust/admin/templates/install.html piecrust/admin/templates/list_source.html piecrust/admin/views/create.py piecrust/admin/views/dashboard.py piecrust/admin/views/edit.py piecrust/admin/views/menu.py piecrust/admin/views/preview.py piecrust/admin/views/publish.py piecrust/admin/views/sources.py piecrust/admin/web.py piecrust/commands/builtin/admin.py piecrust/data/assetor.py piecrust/data/paginator.py
diffstat 19 files changed, 343 insertions(+), 467 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/admin/blueprint.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/blueprint.py	Tue Jun 20 21:13:08 2017 -0700
@@ -1,9 +1,7 @@
 import time
 import logging
-from flask import Blueprint, current_app, g, request
-from .configuration import (
-    FoodTruckConfigNotFoundError, get_foodtruck_config)
-from .sites import FoodTruckSites, InvalidSiteError
+from flask import Blueprint, current_app, g
+from .siteinfo import SiteInfo
 
 
 logger = logging.getLogger(__name__)
@@ -33,9 +31,9 @@
 
 
 def record_login_manager(state):
-    if state.app.secret_key == 'temp-key':
+    if state.app.config['SECRET_KEY'] == 'temp-key':
         def _handler():
-            raise FoodTruckConfigNotFoundError()
+            raise Exception("No secret key has been set!")
 
         logger.debug("No secret key found, disabling website login.")
         login_manager.unauthorized_handler(_handler)
@@ -92,31 +90,12 @@
 
 @foodtruck_bp.before_request
 def _setup_foodtruck_globals():
-    def _get_config():
-        admin_root = current_app.config['FOODTRUCK_ROOT']
-        procedural_config = current_app.config['FOODTRUCK_PROCEDURAL_CONFIG']
-        return get_foodtruck_config(admin_root, procedural_config)
-
-    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.")
+    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)
 
-        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
-
-    def _get_current_site():
-        return g.sites.get()
-
-    g.config = LazySomething(_get_config)
-    g.sites = LazySomething(_get_sites)
-    g.site = LazySomething(_get_current_site)
+    g.site = LazySomething(_get_site)
 
 
 @foodtruck_bp.after_request
--- a/piecrust/admin/configuration.py	Tue Jun 20 21:12:35 2017 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-import os.path
-import copy
-import logging
-import yaml
-from piecrust.configuration import (
-    Configuration, ConfigurationError, ConfigurationLoader,
-    merge_dicts)
-
-
-logger = logging.getLogger(__name__)
-
-
-def get_foodtruck_config(dirname=None, fallback_config=None):
-    dirname = dirname or os.getcwd()
-    cfg_path = os.path.join(dirname, 'foodtruck.yml')
-    return FoodTruckConfiguration(cfg_path, fallback_config)
-
-
-class FoodTruckConfigNotFoundError(Exception):
-    pass
-
-
-class FoodTruckConfiguration(Configuration):
-    def __init__(self, cfg_path, fallback_config=None):
-        super(FoodTruckConfiguration, self).__init__()
-        self.cfg_path = cfg_path
-        self.fallback_config = fallback_config
-
-    def _load(self):
-        try:
-            with open(self.cfg_path, 'r', encoding='utf-8') as fp:
-                values = yaml.load(
-                    fp.read(),
-                    Loader=ConfigurationLoader)
-
-            self._values = self._validateAll(values)
-        except OSError:
-            if self.fallback_config is None:
-                raise FoodTruckConfigNotFoundError()
-
-            logger.debug("No FoodTruck configuration found, using fallback "
-                         "configuration.")
-            self._values = copy.deepcopy(self.fallback_config)
-        except Exception as ex:
-            raise ConfigurationError(
-                "Error loading configuration from: %s" %
-                self.cfg_path) from ex
-
-    def _validateAll(self, values):
-        if values is None:
-            values = {}
-
-        values = merge_dicts(copy.deepcopy(default_configuration), values)
-
-        return values
-
-    def save(self):
-        with open(self.cfg_path, 'w', encoding='utf8') as fp:
-            self.cfg.write(fp)
-
-
-default_configuration = {
-    'triggers': {
-        'bake': 'chef bake'
-    },
-    'scm': {
-        'type': 'hg'
-    },
-    'security': {
-        'username': '',
-        'password': ''
-    }
-}
-
--- a/piecrust/admin/main.py	Tue Jun 20 21:12:35 2017 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-import logging
-
-
-logger = logging.getLogger(__name__)
-
-
-def run_foodtruck(host=None, port=None, debug=False, extra_settings=None):
-    es = {}
-    if debug:
-        es['DEBUG'] = True
-    if extra_settings:
-        es.update(extra_settings)
-
-    from .web import create_foodtruck_app
-    try:
-        app = create_foodtruck_app(es)
-        app.run(host=host, port=port, debug=debug, threaded=True)
-    except SystemExit:
-        # This is needed for Werkzeug's code reloader to be able to correctly
-        # shutdown the child process in order to restart it (otherwise, SSE
-        # generators will keep it alive).
-        from . import pubutil
-        logger.debug("Shutting down SSE generators from main...")
-        pubutil.server_shutdown = True
-        raise
-
--- a/piecrust/admin/settings.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/settings.py	Tue Jun 20 21:13:08 2017 -0700
@@ -0,0 +1,2 @@
+FOODTRUCK_URL_PREFIX = '/pc-admin'
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/admin/siteinfo.py	Tue Jun 20 21:13:08 2017 -0700
@@ -0,0 +1,91 @@
+import os
+import os.path
+import sys
+import copy
+import logging
+import threading
+import subprocess
+from piecrust.app import PieCrustFactory
+
+
+logger = logging.getLogger(__name__)
+
+
+class UnauthorizedSiteAccessError(Exception):
+    pass
+
+
+class InvalidSiteError(Exception):
+    pass
+
+
+class SiteInfo:
+    def __init__(self, root_dir, url_prefix, *, 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 piecrust_factory(self):
+        if self._piecrust_factory is None:
+            self._piecrust_factory = PieCrustFactory(
+                self.root_dir,
+                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%')
+                ])
+        return self._piecrust_factory
+
+    @property
+    def piecrust_app(self):
+        if self._piecrust_app is None:
+            logger.debug("Creating PieCrust admin app: %s" % self.root_dir)
+            self._piecrust_app = self.piecrust_factory.create()
+        return self._piecrust_app
+
+    @property
+    def scm(self):
+        if self._scm is None:
+            cfg = copy.deepcopy(self.piecrust_app.config.get('scm', {}))
+
+            if os.path.isdir(os.path.join(self.root_dir, '.hg')):
+                from .scm.mercurial import MercurialSourceControl
+                self._scm = MercurialSourceControl(self.root_dir, cfg)
+            elif os.path.isdir(os.path.join(self.root_dir, '.git')):
+                from .scm.git import GitSourceControl
+                self._scm = GitSourceControl(self.root_dir, cfg)
+            else:
+                self._scm = False
+
+        return self._scm
+
+    @property
+    def publish_pid_file(self):
+        return os.path.join(self.piecrust_app.cache_dir, 'publish.pid')
+
+    @property
+    def publish_log_file(self):
+        return os.path.join(self.piecrust_app.cache_dir, 'publish.log')
+
+    def publish(self, target):
+        args = [
+            sys.executable, sys.argv[0],
+            '--pid-file', self.publish_pid_file,
+            'publish',
+            '--log-publisher', self.publish_log_file,
+            target]
+        logger.debug("Running publishing command: %s" % args)
+        proc = subprocess.Popen(args, cwd=self.root_dir)
+
+        def _comm():
+            proc.communicate()
+
+        t = threading.Thread(target=_comm, daemon=True)
+        t.start()
+
--- a/piecrust/admin/sites.py	Tue Jun 20 21:12:35 2017 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-import os
-import os.path
-import copy
-import logging
-import threading
-import subprocess
-from piecrust.app import PieCrust
-from piecrust.configuration import merge_dicts
-
-
-logger = logging.getLogger(__name__)
-
-
-class UnauthorizedSiteAccessError(Exception):
-    pass
-
-
-class InvalidSiteError(Exception):
-    pass
-
-
-class Site(object):
-    def __init__(self, name, root_dir, config):
-        self.name = name
-        self.root_dir = root_dir
-        self._global_config = config
-        self._piecrust_app = None
-        self._scm = None
-        logger.debug("Creating site object for %s" % self.name)
-
-    @property
-    def piecrust_app(self):
-        if self._piecrust_app is None:
-            s = PieCrust(self.root_dir)
-            s.config.set('site/root', '/site/%s/' % self.name)
-            self._piecrust_app = s
-        return self._piecrust_app
-
-    @property
-    def scm(self):
-        if self._scm is None:
-            cfg = copy.deepcopy(self._global_config.get('scm', {}))
-            merge_dicts(cfg, self.piecrust_app.config.get('scm', {}))
-
-            if os.path.isdir(os.path.join(self.root_dir, '.hg')):
-                from .scm.mercurial import MercurialSourceControl
-                self._scm = MercurialSourceControl(self.root_dir, cfg)
-            elif os.path.isdir(os.path.join(self.root_dir, '.git')):
-                from .scm.git import GitSourceControl
-                self._scm = GitSourceControl(self.root_dir, cfg)
-            else:
-                self._scm = False
-
-        return self._scm
-
-    @property
-    def publish_pid_file(self):
-        return os.path.join(self.piecrust_app.cache_dir, 'publish.pid')
-
-    @property
-    def publish_log_file(self):
-        return os.path.join(self.piecrust_app.cache_dir, 'publish.log')
-
-    def publish(self, target):
-        args = [
-            'chef',
-            '--pid-file', self.publish_pid_file,
-            'publish', target,
-            '--log-publisher', self.publish_log_file]
-        proc = subprocess.Popen(args, cwd=self.root_dir)
-
-        def _comm():
-            proc.communicate()
-
-        t = threading.Thread(target=_comm, daemon=True)
-        t.start()
-
-
-class FoodTruckSites():
-    def __init__(self, config, current_site):
-        self._sites = {}
-        self.config = config
-        self.current_site = current_site
-        if current_site is None:
-            raise Exception("No current site was given.")
-
-    def get_root_dir(self, name=None):
-        name = name or self.current_site
-        root_dir = self.config.get('sites/%s' % name)
-        if root_dir is None:
-            raise InvalidSiteError("No such site: %s" % name)
-        if not os.path.isdir(root_dir):
-            raise InvalidSiteError("Site '%s' has an invalid path." % name)
-        return root_dir
-
-    def get(self, name=None):
-        name = name or self.current_site
-        s = self._sites.get(name)
-        if s:
-            return s
-
-        root_dir = self.get_root_dir(name)
-        s = Site(name, root_dir, self.config)
-        self._sites[name] = s
-        return s
-
-    def getall(self):
-        for name in self.config.get('sites'):
-            yield self.get(name)
-
--- a/piecrust/admin/templates/install.html	Tue Jun 20 21:12:35 2017 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-{% set title = 'Configuration File Missing' %}
-
-{% extends 'layouts/master.html' %}
-
-{% block content %}
-<p>No FoodTruck configuration file was found. Did you run <code>chef admin init</code>?</p>
-{% endblock %}
-
--- a/piecrust/admin/templates/list_source.html	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/templates/list_source.html	Tue Jun 20 21:13:08 2017 -0700
@@ -14,7 +14,12 @@
     <tbody>
         {% for p in pages %}
         <tr>
-            <td><time class="timeago" datetime="{{p.timestamp|iso8601}}">{{p.timestamp|datetime}}</time></td>
+            <td>{% if p.timestamp > 0 %}
+                <time class="timeago" datetime="{{p.timestamp|iso8601}}">{{p.timestamp|datetime}}</time>
+                {% else %}
+                <em>no date/time</em>
+                {% endif %}
+            </td>
             <td><a href="{{p.url}}">{{p.title}}</a></td>
             <td>{{p.author}}</td>
             <td>{{p.category}}</td>
--- a/piecrust/admin/views/create.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/views/create.py	Tue Jun 20 21:13:08 2017 -0700
@@ -1,11 +1,9 @@
-import os
-import os.path
 import logging
 from flask import (
     g, request, abort, render_template, url_for, redirect, flash)
 from flask.ext.login import login_required
+from piecrust.page import Page
 from piecrust.sources.interfaces import IInteractiveSource
-from piecrust.routing import create_route_metadata
 from ..blueprint import foodtruck_bp
 from ..views import with_menu_context
 
@@ -16,8 +14,8 @@
 @foodtruck_bp.route('/write/<source_name>', methods=['GET', 'POST'])
 @login_required
 def write_page(source_name):
-    site = g.site.piecrust_app
-    source = site.getSource(source_name)
+    pcapp = g.site.piecrust_app
+    source = pcapp.getSource(source_name)
     if source is None:
         abort(400)
     if not isinstance(source, IInteractiveSource):
@@ -25,52 +23,11 @@
 
     if request.method == 'POST':
         if 'do_save' in request.form:
-            metadata = {}
-            for f in source.getInteractiveFields():
-                metadata[f.name] = f.default_value
-            for fk, fv in request.form.items():
-                if fk.startswith('meta-'):
-                    metadata[fk[5:]] = fv
-
-            logger.debug("Searching for page with metadata: %s" % metadata)
-            fac = source.findPageFactory(metadata, MODE_CREATING)
-            if fac is None:
-                logger.error("Can't find page for %s" % metadata)
-                abort(500)
-
-            logger.debug("Creating page: %s" % fac.path)
-            os.makedirs(os.path.dirname(fac.path), exist_ok=True)
-            with open(fac.path, 'w', encoding='utf8') as fp:
-                fp.write('')
-            flash("%s was created." % os.path.relpath(fac.path, site.root_dir))
-
-            route = site.getSourceRoute(source.name, fac.metadata)
-            if route is None:
-                logger.error("Can't find route for page: %s" % fac.path)
-                abort(500)
-
-            dummy = _DummyPage(fac)
-            route_metadata = create_route_metadata(dummy)
-            uri = route.getUri(route_metadata)
-            uri_root = '/site/%s/' % g.site.name
-            uri = uri[len(uri_root):]
-            logger.debug("Redirecting to: %s" % uri)
-
-            return redirect(url_for('.edit_page', slug=uri))
-
+            return _submit_page_form(pcapp, source)
         abort(400)
-
     return _write_page_form(source)
 
 
-class _DummyPage:
-    def __init__(self, fac):
-        self.source_metadata = fac.metadata
-
-    def getRouteMetadata(self):
-        return {}
-
-
 def _write_page_form(source):
     data = {}
     data['is_new_page'] = True
@@ -87,3 +44,42 @@
     with_menu_context(data)
     return render_template('create_page.html', **data)
 
+
+def _submit_page_form(pcapp, source):
+    metadata = {}
+    for f in source.getInteractiveFields():
+        metadata[f.name] = f.default_value
+    for fk, fv in request.form.items():
+        if fk.startswith('meta-'):
+            metadata[fk[5:]] = fv
+
+    logger.debug("Searching for item with metadata: %s" % metadata)
+    content_item = source.findContent(metadata)
+    if content_item is None:
+        logger.error("Can't find item for: %s" % metadata)
+        abort(500)
+
+    logger.debug("Creating item: %s" % content_item.spec)
+    with source.openItem(content_item, mode='w') as fp:
+        fp.write('')
+    flash("'%s' was created." % content_item.spec)
+
+    route = pcapp.getSourceRoute(source.name)
+    if route is None:
+        logger.error("Can't find route for source: %s" % source.name)
+        abort(500)
+
+    page = Page(source, content_item)
+    uri = page.getUri()
+    logger.debug("Redirecting to: %s" % uri)
+    return redirect(url_for('.edit_page', uri=uri))
+
+
+class _DummyPage:
+    def __init__(self, fac):
+        self.source_metadata = fac.metadata
+
+    def getRouteMetadata(self):
+        return {}
+
+
--- a/piecrust/admin/views/dashboard.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/views/dashboard.py	Tue Jun 20 21:13:08 2017 -0700
@@ -6,7 +6,7 @@
     render_template, url_for, redirect)
 from flask.ext.login import login_user, logout_user, login_required
 from piecrust.configuration import parse_config_header
-from piecrust.rendering import QualifiedPage
+from piecrust.sources.interfaces import IInteractiveSource
 from piecrust.uriutil import split_uri
 from ..textutil import text_preview
 from ..blueprint import foodtruck_bp, load_user, after_this_request
@@ -21,16 +21,22 @@
 def index():
     data = {}
     data['sources'] = []
-    site = g.site
+
     fs_endpoints = {}
-    for source in site.piecrust_app.sources:
+
+    site = g.site
+    pcapp = site.piecrust_app
+    for source in pcapp.sources:
         if source.is_theme_source:
             continue
-        facs = source.getPageFactories()
+        if not isinstance(source, IInteractiveSource):
+            continue
+
+        items = source.getAllContents()
         src_data = {
             'name': source.name,
             'list_url': url_for('.list_source', source_name=source.name),
-            'page_count': len(facs)}
+            'page_count': len(items)}
         data['sources'].append(src_data)
 
         fe = getattr(source, 'fs_endpoint', None)
@@ -55,20 +61,9 @@
             else:
                 data['misc_files'].append(p)
 
-    data['site_name'] = site.name
-    data['site_title'] = site.piecrust_app.config.get('site/title', site.name)
+    data['site_title'] = pcapp.config.get('site/title', "Unnamed Website")
     data['url_publish'] = url_for('.publish')
-    data['url_preview'] = url_for('.preview_site_root', sitename=site.name)
-
-    data['sites'] = []
-    for s in g.sites.getall():
-        data['sites'].append({
-            'name': s.name,
-            'display_name': s.piecrust_app.config.get('site/title'),
-            'url': url_for('.index', site_name=s.name)
-        })
-    data['needs_switch'] = len(g.config.get('sites')) > 1
-    data['url_switch'] = url_for('.switch_site')
+    data['url_preview'] = url_for('.preview_root_page')
 
     with_menu_context(data)
     return render_template('dashboard.html', **data)
--- a/piecrust/admin/views/edit.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/views/edit.py	Tue Jun 20 21:13:08 2017 -0700
@@ -13,59 +13,85 @@
 logger = logging.getLogger(__name__)
 
 
-@foodtruck_bp.route('/edit/', defaults={'slug': ''}, methods=['GET', 'POST'])
-@foodtruck_bp.route('/edit/<path:slug>', methods=['GET', 'POST'])
+@foodtruck_bp.route('/edit/', defaults={'uri': ''}, methods=['GET', 'POST'])
+@foodtruck_bp.route('/edit/<path:uri>', methods=['GET', 'POST'])
 @login_required
-def edit_page(slug):
+def edit_page(uri):
     site = g.site
-    site_app = site.piecrust_app
-    rp = get_requested_page(site_app,
-                            '/site/%s/%s' % (g.sites.current_site, slug))
-    page = rp.qualified_page
+    pcapp = site.piecrust_app
+    rp = get_requested_page(pcapp, '%s/preview/%s' % (site.url_prefix, uri))
+    page = rp.page
     if page is None:
         abort(404)
 
     if request.method == 'POST':
-        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 _submit_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:
-                site.scm.commit([page.path], 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)
+    return _edit_page_form(page, uri)
 
 
-@foodtruck_bp.route('/upload/<path:slug>', methods=['POST'])
-def upload_page_asset(slug):
+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 i, n in enumerate(assetor._getAssetNames()):
+        assets_data.append({'name': n, 'url': assetor[i]})
+    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):
     if 'ft-asset-file' not in request.files:
-        return redirect(url_for('.edit_page', slug=slug))
+        return redirect(url_for('.edit_page', uri=uri))
 
     asset_file = request.files['ft-asset-file']
     if asset_file.filename == '':
-        return redirect(url_for('.edit_page', slug=slug))
+        return redirect(url_for('.edit_page', uri=uri))
 
     site = g.site
-    site_app = site.piecrust_app
-    rp = get_requested_page(site_app,
-                            '/site/%s/%s' % (g.sites.current_site, slug))
+    pcapp = site.piecrust_app
+    rp = get_requested_page(pcapp,
+                            '/site/%s/%s' % (g.sites.current_site, uri))
     page = rp.qualified_page
     if page is None:
         abort(404)
@@ -83,29 +109,4 @@
     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', 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)
-
+    return redirect(url_for('.edit_page', uri=uri))
--- a/piecrust/admin/views/menu.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/views/menu.py	Tue Jun 20 21:13:08 2017 -0700
@@ -1,5 +1,6 @@
 from flask import g, request, url_for
 from flask.ext.login import current_user
+from piecrust.sources.interfaces import IInteractiveSource
 
 
 def get_menu_context():
@@ -10,22 +11,29 @@
         'icon': 'speedometer'})
 
     site = g.site.piecrust_app
-    for s in site.sources:
-        if s.is_theme_source:
+    for source in site.sources:
+        if source.is_theme_source:
+            continue
+        if not isinstance(source, IInteractiveSource):
             continue
 
-        source_icon = s.config.get('admin_icon', 'document')
-        if s.name == 'pages':
-            source_icon = 'document-text'
-        elif 'blog' in s.name:
-            source_icon = 'filing'
+        # Figure out the icon to use... we do some hard-coded stuff to
+        # have something vaguely pretty out of the box.
+        source_icon = source.config.get('admin_icon')
+        if source_icon is None:
+            if source.name == 'pages':
+                source_icon = 'document-text'
+            elif 'blog' in source.name or 'posts' in source.name:
+                source_icon = 'filing'
+            else:
+                source_icon = 'document'
 
-        url_write = url_for('.write_page', source_name=s.name)
-        url_listall = url_for('.list_source', source_name=s.name)
+        url_write = url_for('.write_page', source_name=source.name)
+        url_listall = url_for('.list_source', source_name=source.name)
 
         ctx = {
             'url': url_listall,
-            'title': s.name,
+            'title': source.name,
             'icon': source_icon,
             'quicklink': {
                 'icon': 'plus-round',
@@ -44,6 +52,7 @@
         'title': "Publish",
         'icon': 'upload'})
 
+    # TODO: re-enable settings UI at some point.
     # entries.append({
     #     'url': url_for('.settings'),
     #     'title': "Settings",
--- a/piecrust/admin/views/preview.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/views/preview.py	Tue Jun 20 21:13:08 2017 -0700
@@ -1,25 +1,21 @@
 from flask import current_app, g, make_response
 from flask.ext.login import login_required
-from piecrust.app import PieCrustFactory
 from piecrust.serving.server import Server
 from ..blueprint import foodtruck_bp
 
 
-@foodtruck_bp.route('/site/<sitename>/')
+@foodtruck_bp.route('/preview/')
 @login_required
-def preview_site_root(sitename):
-    return preview_site(sitename, '/')
+def preview_root_page():
+    return preview_page('/')
 
 
-@foodtruck_bp.route('/site/<sitename>/<path:url>')
+@foodtruck_bp.route('/preview/<path:url>')
 @login_required
-def preview_site(sitename, url):
-    root_dir = g.sites.get_root_dir(sitename)
-    appfactory = PieCrustFactory(
-        root_dir,
-        cache_key='foodtruck',
-        debug=current_app.debug)
-    server = Server(appfactory,
-                    root_url='/site/%s/' % sitename)
+def preview_page(url):
+    pcappfac = g.site.piecrust_factory
+    url_prefix = current_app.config['FOODTRUCK_URL_PREFIX']
+    server = Server(pcappfac,
+                    root_url='%s/preview/' % url_prefix)
     return make_response(server._run_request)
 
--- a/piecrust/admin/views/publish.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/views/publish.py	Tue Jun 20 21:13:08 2017 -0700
@@ -23,13 +23,14 @@
     site = g.site
     pub_cfg = copy.deepcopy(site.piecrust_app.config.get('publish', {}))
     if not pub_cfg:
-        data = {'error': "There are not publish targets defined in your "
+        data = {'error': "There are no publish targets defined in your "
                          "configuration file."}
         return render_template('error.html', **data)
 
     data = {}
     data['url_run'] = url_for('.publish')
-    data['site_title'] = site.piecrust_app.config.get('site/title', site.name)
+    data['site_title'] = site.piecrust_app.config.get('site/title',
+                                                      "Unnamed Website")
     data['targets'] = []
     for tn in sorted(pub_cfg.keys()):
         tc = pub_cfg[tn]
--- a/piecrust/admin/views/sources.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/views/sources.py	Tue Jun 20 21:13:08 2017 -0700
@@ -18,17 +18,16 @@
     default_author = site.config.get('site/author')
     data = {'title': "List %s" % source_name}
     data['pages'] = []
-    pgn = Paginator(None, source, page_num=page_num, items_per_page=20)
+    pgn = Paginator(source, None, sub_num=page_num, items_per_page=20)
     for p in pgn.items:
         page_data = {
             'title': p['title'],
             'author': p.get('author', default_author),
-            'slug': p['slug'],
             'timestamp': p['timestamp'],
             'tags': p.get('tags', []),
             'category': p.get('category'),
             'source': source_name,
-            'url': url_for('.edit_page', slug=p['slug'])
+            'url': url_for('.edit_page', uri=p['slug'])
         }
         data['pages'].append(page_data)
 
--- a/piecrust/admin/web.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/admin/web.py	Tue Jun 20 21:13:08 2017 -0700
@@ -1,69 +1,43 @@
 import os.path
 import logging
-from flask import Flask, render_template
+from flask import Flask
 from werkzeug import SharedDataMiddleware
-from .blueprint import foodtruck_bp
-from .configuration import FoodTruckConfigNotFoundError
-from .sites import InvalidSiteError
 
 
 logger = logging.getLogger(__name__)
 
 
 def create_foodtruck_app(extra_settings=None):
+    from .blueprint import foodtruck_bp
+
     app = Flask(__name__.split('.')[0], static_folder=None)
     app.config.from_object('piecrust.admin.settings')
     app.config.from_envvar('FOODTRUCK_SETTINGS', silent=True)
     if extra_settings:
         app.config.update(extra_settings)
 
-    admin_root = app.config.setdefault('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.
-    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}
-        }
+    root_dir = app.config.setdefault('FOODTRUCK_ROOT', os.getcwd())
 
     # 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')})
+        {'/.well-known': os.path.join(root_dir, '.well-known')})
 
-    if os.path.isfile(config_path):
-        app.config.from_pyfile(config_path)
-
+    # Setup logging/error handling.
     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')
 
-        @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)
-
-    if not app.secret_key:
+    if not app.config['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'
+        app.config['SECRET_KEY'] = 'temp-key'
 
     # Register extensions and blueprints.
-    app.register_blueprint(foodtruck_bp)
+    bp_prefix = app.config['FOODTRUCK_URL_PREFIX']
+    app.register_blueprint(foodtruck_bp, url_prefix=bp_prefix)
 
-    logger.debug("Created FoodTruck app with admin root: %s" % admin_root)
+    logger.debug("Created FoodTruck app with admin root: %s" % root_dir)
 
     return app
 
--- a/piecrust/commands/builtin/admin.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/commands/builtin/admin.py	Tue Jun 20 21:13:08 2017 -0700
@@ -1,7 +1,7 @@
 import os
 import os.path
 import logging
-from piecrust import CACHE_DIR
+from piecrust import CACHE_DIR, CONFIG_PATH
 from piecrust.commands.base import ChefCommand
 from piecrust.pathutil import SiteNotFoundError
 
@@ -22,7 +22,7 @@
         p = subparsers.add_parser(
             'init',
             help="Creates a new administration panel website.")
-        p.set_defaults(sub_func=self._initFoodTruck)
+        p.set_defaults(sub_func=self._initAdminSite)
 
         p = subparsers.add_parser(
             'genpass',
@@ -47,7 +47,15 @@
             help="Don't process and monitor the asset folder(s).",
             dest='monitor_assets',
             action='store_false')
-        p.set_defaults(sub_func=self._runFoodTruck)
+        p.add_argument(
+            '--use-reloader',
+            help="Restart the server when PieCrust code changes",
+            action='store_true')
+        p.add_argument(
+            '--use-debugger',
+            help="Show the debugger when an error occurs",
+            action='store_true')
+        p.set_defaults(sub_func=self._runAdminSite)
 
     def checkedRun(self, ctx):
         if ctx.app.root_dir is None:
@@ -58,29 +66,35 @@
             return
         return ctx.args.sub_func(ctx)
 
-    def _runFoodTruck(self, ctx):
+    def _runAdminSite(self, ctx):
         # See `_run_sse_check` in `piecrust.serving.wrappers` for an
         # explanation of this check.
         if (ctx.args.monitor_assets and (
-                not ctx.args.debug or
+                not (ctx.args.debug or ctx.args.use_reloader) or
                 os.environ.get('WERKZEUG_RUN_MAIN') == 'true')):
             from piecrust.serving.procloop import ProcessingLoop
             out_dir = os.path.join(
-                ctx.app.root_dir, CACHE_DIR, 'foodtruck', 'server')
+                ctx.app.root_dir, CACHE_DIR, 'admin', 'server')
             proc_loop = ProcessingLoop(ctx.appfactory, out_dir)
             proc_loop.start()
 
         es = {
             'FOODTRUCK_CMDLINE_MODE': True,
-            'FOODTRUCK_ROOT': ctx.app.root_dir}
-        from piecrust.admin.main import run_foodtruck
+            'FOODTRUCK_ROOT': ctx.app.root_dir,
+            'FOODTRUCK_URL_PREFIX': '',
+            'SECRET_KEY': os.urandom(22),
+            'LOGIN_DISABLED': True}
+        if ctx.args.debug or ctx.args.use_debugger:
+            es['DEBUG'] = True
+
         run_foodtruck(
             host=ctx.args.address,
             port=ctx.args.port,
-            debug=ctx.args.debug,
+            use_reloader=ctx.args.use_reloader,
             extra_settings=es)
 
-    def _initFoodTruck(self, ctx):
+    def _initAdminSite(self, ctx):
+        import io
         import getpass
         from piecrust.admin import bcryptfallback as bcrypt
 
@@ -89,9 +103,10 @@
         admin_password = getpass.getpass("Admin password: ")
         if not admin_password:
             logger.warning("No administration password set!")
-            logger.warning("Don't make this instance of FoodTruck public.")
+            logger.warning("Don't make this instance of the PieCrust "
+                           "administration panel public.")
             logger.info("You can later set an admin password by editing "
-                        "the `foodtruck.yml` file and using the "
+                        "the `admin.cfg` file and using the "
                         "`chef admin genpass` command.")
         else:
             binpw = admin_password.encode('utf8')
@@ -99,28 +114,54 @@
             admin_password = hashpw
 
         ft_config = """
-security:
+admin:
+    secret_key: %(secret_key)s
     username: %(username)s
     # You can generate another hashed password with `chef admin genpass`.
     password: %(password)s
 """
         ft_config = ft_config % {
+            'secret_key': secret_key,
             'username': admin_username,
             'password': admin_password
         }
-        with open('foodtruck.yml', 'w', encoding='utf8') as fp:
+
+        config_path = os.path.join(ctx.app.root_dir, CONFIG_PATH)
+        with open(config_path, 'a+', encoding='utf8') as fp:
+            fp.seek(0, io.SEEK_END)
+            fp.write('\n')
             fp.write(ft_config)
 
-        flask_config = """
-SECRET_KEY = %(secret_key)s
-"""
-        flask_config = flask_config % {'secret_key': secret_key}
-        with open('app.cfg', 'w', encoding='utf8') as fp:
-            fp.write(flask_config)
-
     def _generatePassword(self, ctx):
         from piecrust.admin import bcryptfallback as bcrypt
         binpw = ctx.args.password.encode('utf8')
         hashpw = bcrypt.hashpw(binpw, bcrypt.gensalt()).decode('utf8')
         logger.info(hashpw)
 
+
+def run_foodtruck(host=None, port=None, use_reloader=False,
+                  extra_settings=None):
+    es = {}
+    if extra_settings:
+        es.update(extra_settings)
+
+    # Disable PIN protection with Werkzeug's debugger.
+    os.environ['WERKZEUG_DEBUG_PIN'] = 'off'
+
+    try:
+        from piecrust.admin.web import create_foodtruck_app
+        app = create_foodtruck_app(es)
+        app.run(host=host, port=port, use_reloader=use_reloader,
+                threaded=True)
+    except SystemExit:
+        # This is needed for Werkzeug's code reloader to be able to correctly
+        # shutdown the child process in order to restart it (otherwise, SSE
+        # generators will keep it alive).
+        try:
+            from . import pubutil
+            logger.debug("Shutting down SSE generators from main...")
+            pubutil.server_shutdown = True
+        except ImportError:
+            pass
+        raise
+
--- a/piecrust/data/assetor.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/data/assetor.py	Tue Jun 20 21:13:08 2017 -0700
@@ -45,6 +45,10 @@
         self._cacheAssets()
         return len(self._cache_list)
 
+    def _getAssetNames(self):
+        self._cacheAssets()
+        return self._cache_map.keys()
+
     def _getAssetItems(self):
         self._cacheAssets()
         return map(lambda i: i.content_item, self._cache_map.values())
--- a/piecrust/data/paginator.py	Tue Jun 20 21:12:35 2017 -0700
+++ b/piecrust/data/paginator.py	Tue Jun 20 21:13:08 2017 -0700
@@ -216,7 +216,8 @@
 
     def _onIteration(self, it):
         if not self._pgn_set_on_ctx:
-            rcs = self._page.app.env.render_ctx_stack
-            rcs.current_ctx.setPagination(self)
-            self._pgn_set_on_ctx = True
+            rcs = self._source.app.env.render_ctx_stack
+            if rcs.current_ctx is not None:
+                rcs.current_ctx.setPagination(self)
+                self._pgn_set_on_ctx = True