Mercurial > piecrust2
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