Mercurial > piecrust2
changeset 655:9c0092fae31d
Merge pull request #26 from GitHub.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Mon, 22 Feb 2016 22:50:30 -0800 |
parents | 466bbddd121e (diff) b1c4c9e0c65f (current diff) |
children | dba53f0f7671 |
files | |
diffstat | 132 files changed, 4600 insertions(+), 1171 deletions(-) [+] |
line wrap: on
line diff
--- a/.ctrlpignore Wed Dec 30 20:21:41 2015 +1300 +++ b/.ctrlpignore Mon Feb 22 22:50:30 2016 -0800 @@ -1,2 +1,4 @@ dir:build/lib +bower_components +node_modules
--- a/.gutctags Wed Dec 30 20:21:41 2015 +1300 +++ b/.gutctags Mon Feb 22 22:50:30 2016 -0800 @@ -2,8 +2,8 @@ --exclude=dist --exclude=docs/_cache --exclude=docs/_counter ---exclude=docs/bower_components ---exclude=docs/node_modules +--exclude=bower_components +--exclude=node_modules --exclude=docs/raw --exclude=*.egg-info --exclude=__pycache__
--- a/.hgignore Wed Dec 30 20:21:41 2015 +1300 +++ b/.hgignore Mon Feb 22 22:50:30 2016 -0800 @@ -1,16 +1,18 @@ syntax: glob *.pyc *.egg-info +__pycache__ venv tags +bower_components +node_modules build/lib util/messages/_cache util/messages/_counter dist docs/_cache docs/_counter -docs/bower_components -docs/node_modules +foodtruck/static piecrust.egg-info piecrust/__version__.py .ropeproject
--- a/.hgtags Wed Dec 30 20:21:41 2015 +1300 +++ b/.hgtags Mon Feb 22 22:50:30 2016 -0800 @@ -20,3 +20,5 @@ c3c1171679de13432c3d2ce08f7a29cef0f35423 2.0.0b1 03c3a77fda60d28df6d5f3592ed4468b4869bdea 2.0.0b2 6ef89b31dddaf50d8cbcb8ee6603e4530ed4d64c 2.0.0b3 +14eec6faf10bad9e539bfb92c2984155412846a7 2.0.0b4 +cc2d212c3ba14af0ac8c38ce3c2764ed7db6f1fe 2.0.0b5
--- a/CHANGELOG.rst Wed Dec 30 20:21:41 2015 +1300 +++ b/CHANGELOG.rst Mon Feb 22 22:50:30 2016 -0800 @@ -10,7 +10,169 @@ ================================== -1. PieCrust 2.0.0b2 (2015-07-29) +1. PieCrust 2.0.0b5 (2016-02-16) +================================== + + +1.0 Commands +---------------------- + +* admin: Remove settings view. +* admin: Don't require ``bcrypt`` for running FoodTruck with ``chef`` . + +1.1 Core +---------------------- + +* internal: Remove SyntaxWarning from MacOS wrappers. + +1.2 Project +---------------------- + +* cm: Fix CHANGELOG newlines on Windows. +* cm: Update npm modules and bower packages before making a release. +* cm: Fixes and tweaks to the documentation generation task. +* cm: Update node module versions. +* cm: Update the node modules before building the documentation. +* cm: Get a new version of pytest-cov to avoid a random multiprocessing bug. +* cm: Ignore more things for pytest. +* cm: Move all scripts into a ``garcon`` package with ``invoke`` support. +* cm: Exclude the correct directories from vim-gutentags. +* cm: Fix categorization of CHANGELOG entries for new commands. +* cm: Regenerate the CHANGELOG. + +================================== +2. PieCrust 2.0.0b4 (2016-02-09) +================================== + + +1.0 Commands +---------------------- + +* chef: Fix the ``--config-set`` option. +* admin: Make the publish UI handle new kinds of target configurations. +* admin: Fix crashes when creating a new page. +* admin: Fix responsive layout. +* admin: Use ``HGPLAIN`` for the Mercurial VCS provider. +* publish: Add option to change the source for the ``rsync`` publisher. +* publish: Change the ``shell`` config setting name for the command to run. +* publish: Add the ``rsync`` publisher. +* publish: Polish/refactor the publishing workflows. +* admin: Make the sidebar togglable for smaller screens. +* admin: Change the default admin server port to 8090, add ``--port`` option. +* admin: Improve publish logs showing as alerts in the admin panel. +* publish: Make the ``shell`` log update faster by flushing the pipe. +* publish: Add publish command. +* chef: Add ``--pid-file`` option. +* admin: Use the app directory, not the cwd, in case of ``--root`` . +* admin: Configuration changes. +* admin: Fix "Publish started" message showing up multiple times. +* admin: Show the install page if no secret key is available. +* admin: Prompt the user for a commit message when committing a page. +* admin: Fix creating pages. +* admin: Better UI for publishing websites. +* admin: Better error reporting, general clean-up. +* admin: Fix constructor for Mercurial SCM. +* admin: Set the ``DEBUG`` flag before the app runs so we can read it during setup. +* admin: Ability to configure SCM stuff per site. +* admin: Better production config for FoodTruck, provide proper first site. +* admin: Make sure we have a valid default site to start with. +* admin: Dashboard UI cleaning, re-use utility function for page summaries. +* admin: Add summary of page in source listing. +* admin: New ``admin`` command to manage FoodTruck-related things. +* admin: Add "FoodTruck" admin panel from the side experiment project. +* bake: Add new performance timers. +* bake: Add support for a "known" page setting that excludes it from the bake. +* bake: Add option to bake assets for FoodTruck. This is likely temporary. +* sources: Add method to get a page factory from a path. +* sources: Add code to support "interactive" metadata acquisition. +* serve: Make it possible to preview pages with a custom root URL. +* serve: Fix corner cases where the pipeline doesn't run correctly. +* showconfig: Don't crash when the whole config should be shown. +* bake: Don't re-setup logging for workers unless we're sure we need it. +* serve: Fix error reporting when the background pipeline fails. +* chef: Add ``--debug-only`` option to only show debug logging for a given logger. +* routes: Add better support for taxonomy slugification. +* serve: Improve reloading and shutdown of the preview server. +* serve: Don't crash when looking at the debug info in a stand-alone window. +* serve: Improve debug information in the preview server. +* serve: Refactor the server to make pieces usable by the debugging middleware. +* serve: Fix timing information in the debug window. +* serve: Extract some of the server's functionality into WSGI middlewares. +* serve: Rewrite of the Server-Sent Event code for build notifications. +* serve: Werkzeug docs say you need to pass a flag with ``wrap_file`` . +* bake: Add a flag to know which record entries got collapsed from last run. +* bake: Set the flags, don't combine. + +1.1 Core +---------------------- + +* debug: Fix debug window CSS. +* debug: Don't show parentheses on redirected properties. +* debug: Fix how the linker shows children/siblings/etc. in the debug window. +* internal: Some fixes to the new app configuration. +* internal: Refactor the app configuration class. +* cli: More proper argument parsing for the main/root arguments. +* cli: Add ``--no-color`` option. +* internal: Rename ``raw_content`` to ``segments`` since it's what it is. +* bug: Fix a crash when some errors occur during page rendering. +* data: Fix a crash bug when no parent page is set on an iterator. +* bug: Correctly handle root URLs with special characters. +* debug: Fix a crash when rendering debug info for some pages. + +1.2 Project +---------------------- + +* docs: Make FoodTruck screenshots the proper size. +* cm: Add script to generate documentation. +* docs: Add documentation about FoodTruck. +* docs: Add raw files for FoodTruck screenshots. +* docs: Add documentation about the ``publish`` command. +* cm: Add some pretty little icons in the README. +* tests: Add unicode tests for case-sensitive file-systems. +* cm: Merge the 2 foodtruck folders, cleanup. +* cm: Fix Gulp config. +* docs: Fix broken link. +* cm: Put Bower/Gulp/etc. stuff all at the root. +* cm: Add requirements for FoodTruck. +* cm: Ignore more stuff for CtrlP or Gutentags. +* tests: Fix (hopefully) time-sensitive tests. +* cm: CHANGELOG generator can handle future versions. +* docs: Remove LessCSS dependencies in the tutorial, fix typos. +* tests: Fix broken unit test. +* tests: Fix another broken test. +* docs: Add reference entry about the ``site/slugify_mode`` setting. +* tests: Fix broken test. +* tests: Print more information when a bake test fails to find an output file. + +================================== +3. PieCrust 2.0.0b3 (2015-08-01) +================================== + + +1.0 Commands +---------------------- + +* import: Correctly convert unicode characters in site configuration. +* import: Fix the PieCrust 1 importer. +* import: Add some debug logging. + +1.1 Core +---------------------- + +* internal: Fix a severe bug with the file-system wrappers on OSX. +* templating: Make more date functions accept 'now' as an input. + +1.2 Project +---------------------- + +* cm: Update changelog. +* cm: Changelog generator script. +* cm: Add a Gutentags config file for ``ctags`` generation. +* tests: Check accented characters work in configurations. +* cm: Ignore Rope cache. + +================================== +4. PieCrust 2.0.0b2 (2015-07-29) ================================== @@ -25,7 +187,7 @@ * bug: Fix crash running ``chef help scaffolding`` outside of a website. ================================== -2. PieCrust 2.0.0b1 (2015-07-29) +5. PieCrust 2.0.0b1 (2015-07-29) ================================== @@ -104,7 +266,7 @@ * jinja: Support ``.j2`` file extensions. ================================== -3. PieCrust 2.0.0a13 (2015-07-14) +6. PieCrust 2.0.0a13 (2015-07-14) ================================== @@ -120,7 +282,7 @@ * bug: Correctly setup the environment/app for bake workers. ================================== -4. PieCrust 2.0.0a12 (2015-07-14) +7. PieCrust 2.0.0a12 (2015-07-14) ================================== @@ -206,7 +368,7 @@ * markdown: Cache the formatter once. ================================== -5. PieCrust 2.0.0a11 (2015-05-18) +8. PieCrust 2.0.0a11 (2015-05-18) ================================== @@ -240,7 +402,7 @@ * jinja: Look for ``html`` extension first instead of last. ================================== -6. PieCrust 2.0.0a10 (2015-05-15) +9. PieCrust 2.0.0a10 (2015-05-15) ================================== @@ -250,7 +412,7 @@ * setup: Add ``requirements.txt`` to ``MANIFEST.in`` so it can be used by the setup. ================================== -7. PieCrust 2.0.0a9 (2015-05-11) +10. PieCrust 2.0.0a9 (2015-05-11) ================================== @@ -281,7 +443,7 @@ * setup: Keep the requirements in sync between ``setuptools`` and ``pip`` . ================================== -8. PieCrust 2.0.0a8 (2015-05-03) +11. PieCrust 2.0.0a8 (2015-05-03) ================================== @@ -315,7 +477,7 @@ * Update ``requirements.txt`` . ================================== -9. PieCrust 2.0.0a7 (2015-04-20) +12. PieCrust 2.0.0a7 (2015-04-20) ================================== @@ -366,7 +528,7 @@ * cleancss: Fix stupid bug. ================================== -10. PieCrust 2.0.0a6 (2015-03-30) +13. PieCrust 2.0.0a6 (2015-03-30) ================================== @@ -533,7 +695,7 @@ * processing: Add more information to the pipeline record. ================================== -11. PieCrust 2.0.0a5 (2015-01-03) +14. PieCrust 2.0.0a5 (2015-01-03) ==================================
--- a/MANIFEST.in Wed Dec 30 20:21:41 2015 +1300 +++ b/MANIFEST.in Mon Feb 22 22:50:30 2016 -0800 @@ -3,6 +3,8 @@ include LICENSE.rst include requirements.txt include dev-requirements.txt +recursive-include foodtruck *.py *.html +recursive-include foodtruck/static * recursive-include piecrust *.py mime.types recursive-include piecrust/resources * recursive-include tests *.py
--- a/README.rst Wed Dec 30 20:21:41 2015 +1300 +++ b/README.rst Mon Feb 22 22:50:30 2016 -0800 @@ -9,6 +9,20 @@ .. _the official website: http://bolt80.com/piecrust/ +|pypi-version| |pypi-downloads| |build-status| + +.. |pypi-version| image:: https://img.shields.io/pypi/v/piecrust.svg + :target: https://pypi.python.org/pypi/piecrust + :alt: PyPI: the Python Package Index +.. |pypi-downloads| image:: https://img.shields.io/pypi/dm/piecrust.svg + :target: https://pypi.python.org/pypi/piecrust + :alt: PyPI: the Python Package Index +.. |build-status| image:: https://img.shields.io/travis/ludovicchabant/PieCrust2/master.svg + :target: https://travis-ci.org/ludovicchabant/PieCrust2 + :alt: Travis CI: continuous integration status + + + Quickstart ==========
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bower.json Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,21 @@ +{ + "name": "PieCrust", + "description": "", + "version": "0.0.1", + "private": "true", + "authors": [ + "Ludovic Chabant <ludovic@chabant.com>" + ], + "homepage": "http://bolt80.com/piecrust", + "ignore": [ + "node_modules", + "bower_components" + ], + "dependencies": { + "bootstrap": "~3.3.5", + "bootstrap-sass": "~3.3.5", + "font-awesome": "fontawesome#~4.5.0", + "Ionicons": "ionicons#~2.0.1" + } +} +
--- a/dev-requirements.txt Wed Dec 30 20:21:41 2015 +1300 +++ b/dev-requirements.txt Mon Feb 22 22:50:30 2016 -0800 @@ -1,7 +1,8 @@ cov-core==1.15.0 -coverage==3.7.1 +coverage==4.0.3 +invoke==0.12.2 mock==1.0.1 -pytest==2.7.0 -pytest-cov==1.8.1 -pytest-mock==0.4.3 +pytest==2.8.7 +pytest-cov==2.2.1 +pytest-mock==0.10.1
--- a/docs/assets/css/piecrust.less Wed Dec 30 20:21:41 2015 +1300 +++ b/docs/assets/css/piecrust.less Mon Feb 22 22:50:30 2016 -0800 @@ -1,29 +1,31 @@ // Imports +@bootstrap-path: "../../../bower_components/bootstrap"; + // Core variables and mixins -@import "../../bower_components/bootstrap/less/variables.less"; -@import "../../bower_components/bootstrap/less/mixins.less"; +@import "@{bootstrap-path}/less/variables.less"; +@import "@{bootstrap-path}/less/mixins.less"; // Reset and dependencies -@import "../../bower_components/bootstrap/less/normalize.less"; -@import "../../bower_components/bootstrap/less/print.less"; -@import "../../bower_components/bootstrap/less/glyphicons.less"; +@import "@{bootstrap-path}/less/normalize.less"; +@import "@{bootstrap-path}/less/print.less"; +@import "@{bootstrap-path}/less/glyphicons.less"; // Core CSS -@import "../../bower_components/bootstrap/less/scaffolding.less"; -@import "../../bower_components/bootstrap/less/type.less"; -@import "../../bower_components/bootstrap/less/code.less"; -@import "../../bower_components/bootstrap/less/grid.less"; -@import "../../bower_components/bootstrap/less/forms.less"; -@import "../../bower_components/bootstrap/less/buttons.less"; +@import "@{bootstrap-path}/less/scaffolding.less"; +@import "@{bootstrap-path}/less/type.less"; +@import "@{bootstrap-path}/less/code.less"; +@import "@{bootstrap-path}/less/grid.less"; +@import "@{bootstrap-path}/less/forms.less"; +@import "@{bootstrap-path}/less/buttons.less"; // Components -@import "../../bower_components/bootstrap/less/component-animations.less"; -@import "../../bower_components/bootstrap/less/navs.less"; -@import "../../bower_components/bootstrap/less/navbar.less"; +@import "@{bootstrap-path}/less/component-animations.less"; +@import "@{bootstrap-path}/less/navs.less"; +@import "@{bootstrap-path}/less/navbar.less"; // Utility classes -@import "../../bower_components/bootstrap/less/utilities.less"; -@import "../../bower_components/bootstrap/less/responsive-utilities.less"; +@import "@{bootstrap-path}/less/utilities.less"; +@import "@{bootstrap-path}/less/responsive-utilities.less"; // Variables
--- a/docs/assets/js/piecrust.js.concat Wed Dec 30 20:21:41 2015 +1300 +++ b/docs/assets/js/piecrust.js.concat Mon Feb 22 22:50:30 2016 -0800 @@ -1,7 +1,7 @@ path_mode: absolute files: - - bower_components/jquery/dist/jquery.js - - bower_components/bootstrap/js/transition.js - - bower_components/bootstrap/js/collapse.js - - bower_components/bootstrap/js/scrollspy.js + - ../bower_components/jquery/dist/jquery.js + - ../bower_components/bootstrap/js/transition.js + - ../bower_components/bootstrap/js/collapse.js + - ../bower_components/bootstrap/js/scrollspy.js
--- a/docs/bower.json Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -{ - "name": "PieCrust", - "version": "0.0.1", - "private": "true", - "dependencies": { - "bootstrap": "~3.3.2" - } -}
--- a/docs/docs/02_general/01_chef.md Wed Dec 30 20:21:41 2015 +1300 +++ b/docs/docs/02_general/01_chef.md Mon Feb 22 22:50:30 2016 -0800 @@ -28,7 +28,7 @@ templates, layouts, assets -- into a self-contained static website that you can upload to your public server. -[1]: {{docurl('general/website-structure')}} +[1]: {{docurl('general/creating-websites')}} [2]: {{docurl('content/creating-pages')}}
--- a/docs/docs/10_publishing.md Wed Dec 30 20:21:41 2015 +1300 +++ b/docs/docs/10_publishing.md Mon Feb 22 22:50:30 2016 -0800 @@ -7,41 +7,82 @@ > completely static website**. For how to deploy a PieCrust website as a dynamic > CMS, see the [deployment documentation][deploy]. +To publish your website, you need to "_bake_" it, and then place the output of +this bake on a public server somewhere. + + ## Baking -To publish your content as a static website, you need to "_bake_" it, _i.e._ -generate all the pages, posts, assets, and other pieces of content: +When you "_bake_" your website, PieCrust will generate all the pages, posts, +assets, and other pieces of content you made into simple files on disk. To do +this, simply run: $ chef bake -You should then see some information about how many pages PieCrust baked, how -much time it took to do so, etc. Without any arguments, the output is located -inside the `_counter` directory at the root of your website. +You should see some information about how many pages PieCrust baked, how much +time it took to do so, etc. Without any arguments, the output is placed inside +the `_counter` directory at the root of your website. You can specify another output directory: $ chef bake -o /path/to/my/output -For other parameters, refer to the help page for the `bake` command. +For other parameters, refer to the help page for the `bake` command by running +`chef help bake`. -At this point, you only need to _publish_ it, _i.e._ copy or upload the output -files (everything inside `_counter`, or whatever other output directory you -specified) to a place where people will be able to access them. This is -typically a public directory on machine that will serve it _via_ HTTP using -software like [Apache][] or [Nginx][]. - - -[apache]: http://httpd.apache.org/ -[nginx]: http://nginx.org/ +At this point, you only need to copy or upload the output files (everything +inside `_counter`, or whatever other output directory you specified) to a place +where people will be able to access them. This is typically a public directory +on machine that will serve it _via_ HTTP using software like [Apache][] or +[Nginx][]. ## Publishing -At the moment, there are no publishing features included in PieCrust -- you just -run `chef bake` as mentioned above directly on the server (pointing it to the -public directory), or locally and then upload the output via (S)FTP. +PieCrust is also capable of publishing your website more or less automatically +for the most common types of setup. This is done with the `publish` command. + +You can specify various "_publish targets_" in your website configuration. By +default, a target will first bake your website into a temporary directory, and +then execute some steps that depend on the target type. + +For example, the following configuration has one target (`upload`) that runs +`rsync` to copy the output of the bake to some web server: + +``` +publish: + upload: + type: rsync + destination: user@example.org:/home/user/www +``` + +You can then run: -More publishing features will be included in the future. +``` +$ chef publish upload +Deploying to upload +[ 863.1 ms] baked 4 user pages. +[ 5.3 ms] baked 1 theme pages. +[ 79.2 ms] baked 0 taxonomy pages. +[ 84.8 ms] processed 3 assets. +[ 1210.9 ms] Baked website. +building file list ... done + +sent 29965 bytes received 20 bytes 19990.00 bytes/sec +total size is 22189128 speedup is 740.01 +[ 991.5 ms] Ran publisher rsync +[ 2203.3 ms] Deployed to upload +``` + +Of course, the output will vary based on your website, your target, whether you +previously published that target or wiped the PieCrust cache, etc. + +For more information about the different types of publish targets available in +PieCrust, refer to the [publishers reference][pubs]. + [deploy]: {{docurl('deploying')}} +[pubs]: {{docurl('reference/publishers')}} +[apache]: http://httpd.apache.org/ +[nginx]: http://nginx.org/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/docs/30_admin-panel.md Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,77 @@ +--- +title: Administration Panel +short_title: Admin Panel +--- + +Remember when we said there was no complex administration panel? Well, we kinda +lied. Only not really, since the one that comes with PieCrust is super simple +and, of course, completely optional. That's why we're only bringing it up here. + + + +To run the administration panel, type: + +``` +$ chef admin run + * Running on http://localhost:8090/ (Press CTRL+C to quit) + +``` + +Now copy paste the specified URL (`http://localhost:8090/`) into your favorite +browser's address bar and you should see "FoodTruck", PieCrust's administrative +panel. + +The navigation menu on the left should have: + +* The dashboard. +* One entry for each of your page sources. +* An interface to publish your website. + +## The Dashboard + +This is where you can see a summary of your website, with links to see your list +of already existing content. + +If your website content is also stored in a version control system, you can see +what's currently edited in the "_Work in Progress_" section. + +> Right now, FoodTruck only supports Mercurial for source control. + + +## Page Sources + +For each source, you can list the existing pages: + + + +Clicking on any link will let you edit that page. + +You can also create a new page by clicking the appropriate entry in the left +navigation menu. You'll have to fill up information similar to what you specify +to the `chef prepare` command: + + + +Once you created a page, or click a link for an existing one, you can edit the +page: + + + +Note that if your website is stored in a VCS, you'll have the ability to commit +the page file if you want, but using the dropdown on the "_Save_" button: + + + + +## Publishing + +You also have a UI for running your publish targets. The descriptions shown for +each target are taken from a `description` entry in their configuration +settings. + + + +Clicking any "_Execute_" button will publish your website using the +corresponding target. You'll see some notifications popup on the bottom right to +indicate the progress of the operation. +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/docs/99_reference/06_publishers.md Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,57 @@ +--- +title: "Appendix 6: Publishers" +short_title: "Publishers" +--- + +Here's a list of website publishers that ship by default with PieCrust. + +Publishers are declared and configured in the website configuration like so: + +``` +publish: + <target_name>: + type: <publisher_type> + <config1>: <value1> + <config2>: <value2> +``` + +Note that apart for the `type` setting, all publishers also share a few common +configuration settings: + +* `bake` (`true`): Unless set to `false`, PieCrust will first bake your website + into a temporary folder (`_cache/pub/<target_name>`). The publisher will then + by default pick it up from there. + +In addition to specifying publish targets via configuration settings, you can +also (if you don't need anything fancy) specify some of them via a simple +URL-like line: + +``` +publish: + <target_name>: <something://foo/bar> +``` + +The URL-like format is specified below on a per-publisher basis. + + +## Shell Command + +This simple publisher runs the specified command from inside your website root +directory. + +* `type`: `shell`. +* `command`: The command to run. + + +## Rsync + +This publisher will run `rsync` to copy or upload your website's bake output to +a given destination. + +* `type`: `rsync`. +* `destination`: The destination given to the `rsync` executable. +* `source` (`_cache/pub/<target_name>`): The source given to the `rsync` + executable. It defaults to the automatic pre-publish bake output folder. +* `options` (`-avc --delete`): The options to pass to the `rsync` executable. By + default, those will run `rsync` in "mirroring" mode. +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/assets/js/foodtruck.js Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,65 @@ + +$(document).ready(function() { + $('.ft-nav-toggle').click(function() { + $('.ft-nav-container').toggleClass('ft-nav-enabled'); + $('.ft-nav').toggleClass('ft-nav-enabled'); + }); + + $('.ft-nav-collapsed + ul').hide(); + + $('#ft-commit-modal').on('shown.bs.modal', function () { + $('#ft-commit-msg').focus(); + }); + + var publogEl = $('#ft-publog'); + publogEl.mouseenter(function() { + publogEl.attr('data-autohide', 'false'); + }); + publogEl.on('hide', function() { + var containerEl = $('#ft-publog-container', publogEl); + containerEl.empty(); + }); + + var closePublogBtn = $('button', publogEl); + closePublogBtn.on('click', function() { + publogEl.fadeOut(200); + }); +}); + +var onPublishEvent = function(e) { + + var publogEl = $('#ft-publog'); + var containerEl = $('#ft-publog-container', publogEl); + + var msgEl = $('<div>' + e.data + '</div>'); + var removeMsgEl = function() { + msgEl.remove(); + if (containerEl.children().length == 0) { + // Last message, hide the log window. + publogEl.fadeOut(200); + } + }; + var timeoutId = window.setTimeout(function() { + if (publogEl.attr('data-autohide') == 'true') { + msgEl.fadeOut(400, removeMsgEl); + } + }, 4000); + + if (containerEl.children().length == 0) { + // First message, show the log window, reset the mouseover marker. + publogEl.attr('data-autohide', 'true'); + publogEl.fadeIn(200); + } + containerEl.append(msgEl); +}; + +if (!!window.EventSource) { + var source = new EventSource('/publish-log'); + source.onerror = function(e) { + console.log("Error with SSE, closing.", e); + source.close(); + }; + source.addEventListener('message', onPublishEvent); +} + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/assets/sass/foodtruck.scss Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,44 @@ + +// Overrides +$icon-font-path: '../fonts/'; + +// Core variables and mixins +@import "bootstrap/variables"; +@import "bootstrap/mixins"; + +// Reset and dependencies +@import "bootstrap/normalize"; +@import "bootstrap/print"; +@import "bootstrap/glyphicons"; + +// Core CSS +@import "bootstrap/scaffolding"; +@import "bootstrap/type"; +@import "bootstrap/code"; +@import "bootstrap/grid"; +@import "bootstrap/tables"; +@import "bootstrap/forms"; +@import "bootstrap/buttons"; + +// Components +@import "bootstrap/alerts"; +@import "bootstrap/button-groups"; +@import "bootstrap/close"; +@import "bootstrap/component-animations"; +@import "bootstrap/dropdowns"; +@import "bootstrap/input-groups"; +@import "bootstrap/modals"; + +// Utility classes +@import "bootstrap/utilities"; +@import "bootstrap/responsive-utilities"; + +// Ionicons +@import "ionicons"; + +// Foodtruck +@import "foodtruck/base"; +@import "foodtruck/sidebar"; +@import "foodtruck/editing"; +@import "foodtruck/publog"; +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/assets/sass/foodtruck/_base.scss Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,47 @@ + +$ft-color-gray-darkest: #1A2226; +$ft-color-gray-darker: #1E282C; +$ft-color-gray-dark: #222D32; +$ft-color-gray: #2C3B41; +$ft-color-gray-light: #8AA4AF; +$ft-color-gray-lighter: #B8C7CE; +$ft-color-white: #FFF; +$ft-color-black: #000; +$ft-color-red: #D33939; +$ft-color-blue: #3C8DBC; +$ft-color-yellow: #C9C836; + + +header h1, header.title { + text-align: center; +} + +footer { + text-align: center; + font-size: 0.8em; + letter-spacing: -0.02em; + color: #777; + margin: 4em 2em 2em 2em; +} + +h1, h2, h3, h4, h5, h6 { + margin-bottom: 1em; +} + +.ft-login { + padding: 1em; + margin: 2em 0; + box-shadow: 0 5px 10px #ddd; + border: 1px solid #dcdcdc; +} + +.ft-pagination { + text-align: center; + color: $ft-color-gray; + font-size: 2em; + margin: 1em 0; +} +.ft-pagination-a { + padding: 0 0.2em; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/assets/sass/foodtruck/_editing.scss Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,13 @@ +// +// Page editing +// -------------------------------------------------- + +.ft-write-form textarea { + @include resizable(vertical); + + outline: none; + overflow: auto; + font-family: 'Courier', 'Courier New', sans-serif; + padding: 1em; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/assets/sass/foodtruck/_publog.scss Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,26 @@ + +#ft-publog { + position: fixed; + right: 0; + bottom: 0; + width: 42%; + min-width: 20em; + margin: 0.5em; + color: $ft-color-white; + background: $ft-color-blue; + border-radius: 0.5em; + box-shadow: 0 0 10px darken($ft-color-blue, 50%); + + button { + padding: 0.2em 0.4em; + } +} + +#ft-publog-container { + margin: 1em; + + div { + margin: 0.1em; + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/assets/sass/foodtruck/_sidebar.scss Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,151 @@ +// +// Sidebar navigation +// -------------------------------------------------- + +$ft-nav-width: 25rem; +$ft-nav-margin: 2rem; + +// Layout +.ft-nav-container { + padding: 2rem; + margin-top: 3em; + transition: padding-left 0.5s ease; + transition: margin 0.5s ease; +} +.ft-nav-container.ft-nav-enabled { + padding-left: $ft-nav-width + $ft-nav-margin; +} + +.ft-nav-toggle { + display: block; + z-index: 1001; + position: fixed; + top: 0; + left: 0; + transition: all 0.5s ease; +} + +.ft-nav { + z-index: 1000; + position: fixed; + height: 100%; + width: $ft-nav-width; + left: -$ft-nav-width; + top: 0; + bottom: 0; + overflow-y: auto; + transition: all 0.5s ease; +} +.ft-nav.ft-nav-enabled { + left: 0; +} + +@media(min-width:768px) { + .ft-nav-toggle { + left: -3rem; + display: none; + } + .ft-nav-container { + padding-left: $ft-nav-width + $ft-nav-margin; + margin-top: 0; + } + .ft-nav { + left: 0; + } +} + +// Style +.ft-nav-toggle { + background: $ft-color-gray-darkest; + + a { + display: block; + padding: 0.2em 0.8em; + font-family: 'Lobster', cursive; + font-size: 2em; + } + a, a:hover, a:active, a:visited { + color: #fff; + text-decoration: none; + } +} + +.ft-nav { + background: $ft-color-gray-darkest; + color: #fff; + + span.icon { + font-size: 1.5em; + margin-right: 0.4em; + } +} +.ft-nav ul { + list-style: none; + margin: 0; + padding: 0; +} +.ft-nav ul li { + margin: 0; + padding: 0; +} +.ft-nav ul li a { + border-left: 5px solid transparent; + color: $ft-color-gray-light; + background: $ft-color-gray-dark; + padding: 1rem; + display: block; + letter-spacing: 0.05em; + text-transform: uppercase; + + &:hover { + border-left: 5px solid $ft-color-blue; + color: $ft-color-white; + background: $ft-color-gray-darker; + text-decoration: none; + } +} +.ft-nav li>ul { +} +.ft-nav li>ul li a { + color: $ft-color-gray-lighter; + background: $ft-color-gray; + padding-left: 3em; + text-transform: none; + + &:hover { + color: $ft-color-white; + background: $ft-color-gray; + } +} +.ft-nav ul li a.ft-nav-active { + border-left: 5px solid $ft-color-red; + color: $ft-color-white; + background: $ft-color-gray-darker; +} +.ft-nav li>ul li a.ft-nav-active { + color: $ft-color-white; + background: $ft-color-gray; +} + +// Title/logo +.ft-nav-title { + font-size: 2rem; + font-weight: bold; + text-align: center; + padding: 2rem 0; + margin: 0; +} +.ft-nav-brand { + font-family: 'Lobster', cursive; + font-size: 2em; + text-shadow: 2px 5px 0 $ft-color-gray; +} + +// Footer +.ft-nav-auth { + color: $ft-color-gray; + font-size: 0.8em; + text-align: center; + margin: 2em 0; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/bcryptfallback.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,47 @@ +import hashlib +import logging + + +print_warning = False +logger = logging.getLogger(__name__) + + +try: + from bcrypt import hashpw, gensalt +except ImportError: + print_warning = True + + def hashpw(password, *args, **kwargs): + return hashlib.sha512(password).hexdigest().encode('utf8') + + def gensalt(*args, **kwargs): + return b'' + + +try: + from flask.ext.bcrypt import Bcrypt +except ImportError: + print_warning = True + + def generate_password_hash(password): + return hashlib.sha512(password.encode('utf8')).hexdigest() + + def check_password_hash(reference, check): + check_hash = hashlib.sha512(check.encode('utf8')).hexdigest() + return check_hash == reference + + class SHA512Fallback(object): + is_fallback_bcrypt = True + + def __init__(self, app=None): + self.generate_password_hash = generate_password_hash + self.check_password_hash = check_password_hash + + Bcrypt = SHA512Fallback + + +if print_warning: + logging.warning("Bcrypt not available... falling back to SHA512.") + logging.warning("Run `pip install Flask-Bcrypt` for more secure " + "password hashing.") +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/configuration.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,74 @@ +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': '' + } + } +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/main.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,23 @@ +import logging + + +logger = logging.getLogger(__name__) + + +def run_foodtruck(host=None, port=None, debug=False): + if debug: + import foodtruck.settings + foodtruck.settings.DEBUG = debug + + from .web import app + try: + 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 +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/pubutil.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,159 @@ +import os +import os.path +import time +import errno +import signal +import logging +from .web import app + + +logger = logging.getLogger(__name__) + +server_shutdown = False + + +def _shutdown_server_and_raise_sigint(): + if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true': + # This is needed when hitting CTRL+C to shutdown the Werkzeug server, + # otherwise SSE generators will keep it alive. + logger.debug("Shutting down SSE generators...") + global server_shutdown + server_shutdown = True + raise KeyboardInterrupt() + + +if app.config['FOODTRUCK_CMDLINE_MODE']: + # Make sure CTRL+C works correctly. + signal.signal(signal.SIGINT, + lambda *args: _shutdown_server_and_raise_sigint()) + + +def _read_pid_file(pid_file): + logger.debug("Reading PID file: %s" % pid_file) + try: + with open(pid_file, 'r') as fp: + pid_str = fp.read() + + return int(pid_str.strip()) + except Exception: + logger.error("Error reading PID file.") + raise + + +def _pid_exists(pid): + logger.debug("Checking if process ID %d is running" % pid) + try: + os.kill(pid, 0) + except OSError as ex: + if ex.errno == errno.ESRCH: + # No such process. + return False + elif ex.errno == errno.EPERM: + # No permission, so process exists. + return True + else: + raise + else: + return True + + +class PublishLogReader(object): + _poll_interval = 1 # Check the process every 1 seconds. + _ping_interval = 30 # Send a ping message every 30 seconds. + + def __init__(self, pid_path, log_path): + self.pid_path = pid_path + self.log_path = log_path + + def run(self): + logger.debug("Opening publish log...") + pid = None + pid_mtime = 0 + is_running = False + last_seek = -1 + last_ping_time = 0 + try: + while not server_shutdown: + # PING! + interval = time.time() - last_ping_time + if interval > self._ping_interval: + logger.debug("Sending ping...") + last_ping_time = time.time() + yield bytes("event: ping\ndata: 1\n\n", 'utf8') + + # Check the PID file timestamp. + try: + new_mtime = os.path.getmtime(self.pid_path) + except OSError: + new_mtime = 0 + + # If there's a valid PID file and we either just started + # streaming (pid_mtime == 0) or we remember an older version + # of that PID file (pid_mtime != new_mtime), let's read the + # PID from the file. + is_pid_file_prehistoric = False + if new_mtime > 0 and new_mtime != pid_mtime: + is_pid_file_prehistoric = (pid_mtime == 0) + pid_mtime = new_mtime + pid = _read_pid_file(self.pid_path) + + if is_pid_file_prehistoric: + logger.debug("PID file is pre-historic, we will skip the " + "first parts of the log.") + + # If we have a valid PID, let's check if the process is + # currently running. + was_running = is_running + if pid: + is_running = _pid_exists(pid) + logger.debug( + "Process %d is %s" % + (pid, 'running' if is_running else 'not running')) + if not is_running: + # Let's forget this PID file until it changes. + pid = None + else: + is_running = False + + # Read new data from the log file. + new_data = None + if is_running or was_running: + if last_seek < 0: + # Only send the "publish started" message if we + # actually caught the process as it was starting, not + # if we started streaming after it started. + # This means we saw the PID file get changed. + if not is_pid_file_prehistoric: + outstr = ( + 'event: message\n' + 'data: Publish started.\n\n') + yield bytes(outstr, 'utf8') + last_seek = 0 + + try: + with open(self.log_path, 'r', encoding='utf8') as fp: + fp.seek(last_seek) + new_data = fp.read() + last_seek = fp.tell() + except OSError: + pass + if not is_running: + # Process is not running anymore, let's reset our seek + # marker back to the beginning. + last_seek = -1 + + # Stream the new data to the client, but don't send old stuff + # that happened before we started this stream. + if new_data and not is_pid_file_prehistoric: + logger.debug("SSE: %s" % new_data) + for line in new_data.split('\n'): + outstr = 'event: message\ndata: %s\n\n' % line + yield bytes(outstr, 'utf8') + + time.sleep(self._poll_interval) + + except GeneratorExit: + pass + + logger.debug("Closing publish log...") +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/scm/base.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,23 @@ + + +class RepoStatus(object): + def __init__(self): + self.new_files = [] + self.edited_files = [] + + +class SourceControl(object): + def __init__(self, root_dir, cfg): + self.root_dir = root_dir + self.config = cfg + + def getStatus(self): + raise NotImplementedError() + + def commit(self, paths, message, *, author=None): + author = author or self.config.get('author') + self._doCommit(paths, message, author) + + def _doCommit(self, paths, message, author): + raise NotImplementedError() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/scm/mercurial.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,77 @@ +import os +import logging +import tempfile +import subprocess +from .base import SourceControl, RepoStatus + + +logger = logging.getLogger(__name__) + + +def _s(strs): + """ Convert a byte array to string using UTF8 encoding. """ + if strs is None: + return None + assert isinstance(strs, bytes) + return strs.decode('utf8') + + +class MercurialSourceControl(SourceControl): + def __init__(self, root_dir, cfg): + super(MercurialSourceControl, self).__init__(root_dir, cfg) + self.hg = cfg.get('exe', 'hg') + + def getStatus(self): + res = RepoStatus() + st_out = self._run('status') + for line in st_out.split('\n'): + if len(line) == 0: + continue + if line[0] == '?' or line[0] == 'A': + res.new_files.append(line[2:]) + elif line[0] == 'M': + res.edited_files.append(line[2:]) + return res + + def _doCommit(self, paths, message, author): + if not message: + raise ValueError("No commit message specified.") + + # Check if any of those paths needs to be added. + st_out = self._run('status', *paths) + add_paths = [] + for line in st_out.splitlines(): + if line[0] == '?': + add_paths.append(line[2:]) + if len(add_paths) > 0: + self._run('add', *paths) + + # Create a temp file with the commit message. + f, temp = tempfile.mkstemp() + with os.fdopen(f, 'w') as fd: + fd.write(message) + + # Commit and clean up the temp file. + try: + commit_args = list(paths) + ['-l', temp] + if author: + commit_args += ['-u', author] + self._run('commit', *commit_args) + finally: + os.remove(temp) + + def _run(self, cmd, *args, **kwargs): + exe = [self.hg, '-R', self.root_dir] + exe.append(cmd) + exe += args + + env = dict(os.environ) + env['HGPLAIN'] = 'True' + + logger.debug("Running Mercurial: " + str(exe)) + proc = subprocess.Popen(exe, stdout=subprocess.PIPE, env=env) + out, _ = proc.communicate() + + encoded_out = _s(out) + return encoded_out +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/sites.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,110 @@ +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) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/create_page.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,33 @@ +{% set title = 'Create' %} + +{% extends 'layouts/default.html' %} + +{% block content %} +<form action="{{url_postback}}" method="POST" class="ft-create-form"> + {% for field in fields %} + <div class="row"> + <div class="col-md-10 col-md-offset-1"> + <div class="form-group"> + <div class="input-group input-group-lg"> + <span class="input-group-addon" id="meta-{{field.name}}">{{field.display_name}}</span> + <input type="text" class="form-control" value="{{field.value}}" aria-describedby="meta-{{field.name}}" name="meta-{{field.name}}" /> + </div> + </div> + </div> + </div> + + {% endfor %} + + <input type="hidden" name="source_name" value="{{source_name}}" /> + + <div class="row"> + <div class="col-md-6 col-md-offset-1"> + <a class="btn btn-danger" href="{{url_cancel}}">Cancel</a> + </div> + <div class="col-md-4"> + <button type="submit" name="do_save" class="btn btn-primary btn-lg pull-right">Create and Edit</button> + </div> + </div> +</form> +{% endblock %} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/dashboard.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,60 @@ +{% extends 'layouts/default.html' %} + +{% block content %} +<div class="row"> + <div class="col-md-4 col-md-offset-8"> + {% if needs_switch %} + <form action="{{url_switch}}" method="POST"> + {% for site in sites %} + {% if site.name != site_name %} + <button type="submit" name="site_name" value="{{site.name}}" class="btn"> + <span class="icon ion-shuffle"></span> + Switch to {{site.display_name}}</button> + {% endif %} + {% endfor %} + </form> + {% endif %} + </div> +</div> +<div class="row"> + <div class="col-md-12"> + <h1>{{site_title}} <a href="{{url_preview}}"><span class="icon ion-arrow-right-c"></span></a></h1> + </div> +</div> +<div class="row"> + <div class="col-md-6"> + <h2><span class="icon ion-stats-bars"></span> Site Summary</h2> + {% for s in sources %} + <div class="ft-summary-source"> + <a href="{{s.list_url}}">{{s.page_count}} {{s.name}}</a> + </div> + {% endfor %} + </div> + <div class="col-md-6"> + <h2><span class="icon ion-erlenmeyer-flask"></span> Work in Progress</h2> + {% if new_pages %} + <p>New pages</p> + <ul> + {% for p in new_pages %} + <li><a href="{{p.url}}">{{p.title}}</a><br/> + {%if p.text%}<pre>{{p.text}}</pre>{%endif%}</li> + {% endfor %} + </ul> + {% endif %} + {% if edited_pages %} + <p>Edited pages</p> + <ul> + {% for p in edited_pages %} + <li><a href="{{p.url}}">{{p.title}}</a><br/> + {%if p.text%}<pre>{{p.text}}</pre>{%endif%}</li> + {% endfor %} + </ul> + {% endif %} + {% if not new_pages and not edited_pages %} + <p>No work in progress.</p> + {% endif %} + + </div> +</div> +{% endblock %} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/edit_page.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,61 @@ +{% set title = 'Write' %} + +{% extends 'layouts/default.html' %} + +{% block content %} +<form action="{{url_postback}}" method="POST" class="ft-write-form" id="ft-write-form"> + <div class="row"> + <div class="col-sm-10 col-sm-offset-1"> + <div class="form-group"> + <textarea name="page_text" class="form-control" placeholder="Post contents..." rows="20">{{page_text}}</textarea> + </div> + </div> + </div> + + <input type="hidden" name="is_dos_nl" value="{{is_dos_nl}}" /> + + <div class="row"> + <div class="col-sm-6 col-sm-offset-1 col-xs-8"> + <button type="submit" formtarget="_blank" name="do_preview" class="btn btn-info">Preview</button> + <a class="btn btn-danger" href="{{url_cancel}}">Cancel</a> + </div> + <div class="col-sm-4 col-xs-4"> + <div class="btn-group pull-right"> + <button type="submit" name="do_save" class="btn btn-primary">Save</button> + <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <span class="caret"></span> + <span class="sr-only">Toggle Dropdown</span> + </button> + <ul class="dropdown-menu"> + <li><button type="button" class="btn btn-link" data-toggle="modal" data-target="#ft-commit-modal">Save and Commit</button></li> + </ul> + </div> + </div> + </div> + + <div id="ft-commit-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ft-commit-modal-label"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 class="modal-title" id="ft-commit-modal-label">Commit Page</h4> + </div> + <div class="modal-body"> + <p>This will commit the current page to your source control.</p> + <div class="form-group"> + <div class="input-group"> + <span class="input-group-addon" id="ft-commit-msg-label">Message: </span> + <input type="text" class="form-control" placeholder="{{commit_msg}}" aria-describedby="ft-commit-msg-label" name="commit_msg" id="ft-commit-msg" /> + </div> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + <button type="submit" class="btn btn-primary" name="do_save_and_commit">Save and Commit</button> + </div> + </div> + </div> + </div> +</form> +{% endblock %} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/error.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,9 @@ +{% set title = 'An Error Occured' %} + +{% extends 'layouts/master.html' %} + +{% block content %} +<p>{{error}}</p> +{% endblock %} + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/install.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,8 @@ +{% 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 %} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/layouts/default.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,17 @@ +{% set wrapper_classes = 'ft-nav-container' %} + +{% extends 'layouts/master.html' %} + +{% block after_content %} +<div class="ft-nav-toggle"> + <a href="#">F</a> +</div> +<nav class="ft-nav"> + <div class="ft-nav-title"> + <img src="/static/img/foodtruck.png" alt="Food Truck" /> + <div class="ft-nav-brand">FoodTruck</div> + </div> + {% include 'layouts/menu.html' %} +</nav> +{% endblock %} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/layouts/master.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,43 @@ +<!doctype html> +<html lang=""> + <head> + <meta charset="utf-8"/> + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> + <title>{%if title%}{{title}} – {%endif%}FoodTruck</title> + <meta name="description" content="A PieCrust management dashboard"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <link rel="apple-touch-icon" href="apple-touch-icon.png"/> + <link rel="stylesheet" href="/static/css/foodtruck.min.css"/> + <link href='https://fonts.googleapis.com/css?family=Lobster' rel='stylesheet' type='text/css'/> + </head> + <body> + <div id="ft-wrapper" class="container-fluid {{wrapper_classes}}"> + <header> + {% if title %}<h1 class="title">{{title}}</h1>{% endif %} + {% block sub_header %}{% endblock %} + </header> + {% block before_content %}{% endblock %} + <section> + {% block content %}{% endblock %} + </section> + {% block after_content %}{% endblock %} + <div id="ft-publog" class="ft-publog" role="alert" style="display: none;"> + <button type="button" class="close" aria-label="close"> + <span aria-hidden="true">×</span> + </button> + <div id="ft-publog-container"></div> + </div> + <footer> + <p>Prepared by <a href="http://bolt80.com">BOLT80</a>.</p> + <p>Much <span class="icon ion-heart"></span> to + <a href="http://python.org">Python</a>, + <a href="http://flask.pocoo.org">Flask</a>, + <a href="http://getbootstrap.com">Bootstrap</a>, + <a href="http://ionicons.com/">Ionicons</a>, + and many more.</p> + </footer> + </div> + <script src="/static/js/foodtruck.min.js"></script> + </body> +</html> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/layouts/menu.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,17 @@ +<ul> + {%-for e in menu.entries%} + <li><a href="{{e.url}}" class="{%if e.active%}ft-nav-active{%elif e.entries and not e.open%}ft-nav-collapsed{%endif%}"> + {%-if e.icon%}<span class="icon ion-{{e.icon}}"></span> {%endif%}{{e.title}}</a> + {%-if e.entries%} + <ul> + {%-for e2 in e.entries%} + <li><a href="{{e2.url}}" class="{%if e2.active%}ft-nav-active{%endif%}">{{e2.title}}</a></li> + {%endfor%} + </ul> + {%endif-%} + </li> + {%endfor%} +</ul> +{%if menu.user.is_authenticated%} +<p class="ft-nav-auth">Logged in as {{menu.user.id}}. <a href="{{menu.url_logout}}">Logout</a>.</p> +{%endif%}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/list_source.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,39 @@ +{% extends 'layouts/default.html' %} + +{% block content %} +<div class="row"> + <div class="col-md-12"> + {% for p in pages %} + <div> + <h3><a href="{{p.url}}">{{p.title}}</a></h3> + <p>{{p.text}}</p> + </div> + {% endfor %} + </div> + + {% if pagination.prev_page or pagination.next_page %} + <div class="col-sm-6 col-sm-offset-3"> + <div class="ft-pagination"> + {% if pagination.prev_page %} + <a href="{{pagination.prev_page}}"><span class="icon ion-chevron-left"></span> prev</a> + {% else %} + <span class="icon ion-chevron-left"></span> prev + {% endif %} + + {% for p in pagination.nums %} + {% if p.url %}<a href="{{p.url}}" class="ft-pagination-a">{{p.num}}</a>{% else %}<span class="ft-pagination-a">{{p.num}}</span>{% endif %} + {% endfor %} + + {% if pagination.next_page %} + <a href="{{pagination.next_page}}">next <span class="icon ion-chevron-right"></span></a> + {% else %} + next <span class="icon ion-chevron-right"></span> + {% endif %} + </div> + </div> + {% endif %} +</div> + +{% endblock %} + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/login.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,33 @@ +{% extends 'layouts/master.html' %} + +{% block content %} +<div class="row"> + <div class="col-md-6 col-md-offset-3"> + <div class="ft-login"> + <p class="login-box-msg">{{message|default('You know the drill.')}}</p> + + <form action="{{login_postback}}" method="post"> + <div class="form-group has-feedback"> + <input type="text" name="username" class="form-control" placeholder="Username"> + <span class="form-control-feedback"><i class="icon ion-log-in"></i></span> + </div> + <div class="form-group has-feedback"> + <input type="password" name="password" class="form-control" placeholder="Password"> + <span class="form-control-feedback"><i class="icon ion-locked"></i></span> + </div> + <div class="row"> + <div class="col-xs-8"> + <div class="checkbox"> + <label><input type="checkbox"> Remember Me</input></label> + </div> + </div> + <div class="col-xs-4"> + <button type="submit" class="btn btn-primary btn-block">Log In</button> + </div> + </div> + </form> + </div> + </div> +</div> +{% endblock %} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/templates/publish.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,18 @@ +{% extends 'layouts/default.html' %} + +{% block content %} +<h1>Publish {{site_title}}</h1> + +{% for target in targets %} +<div> + <h3>{{target.name}}</h3> + {% if target.description %}<div>{{target.description}}</div>{% endif %} + <form action="{{url_run}}" method="POST"> + <input type="hidden" name="target" value="{{target.name}}" /> + <button type="submit" class="btn btn-default">Execute</button> + </form> +</div> +{% endfor %} + +{% endblock %} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/textutil.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,41 @@ +from html.parser import HTMLParser + + +def text_preview(txt, length=100, *, max_length=None, offset=0): + max_length = max_length or (length + 50) + extract = txt[offset:offset + length] + if len(txt) > offset + length: + for i in range(offset + length, + min(offset + max_length, len(txt))): + c = txt[i] + if c not in [' ', '\t', '\r', '\n']: + extract += c + else: + extract += '...' + break + return extract + + +class MLStripper(HTMLParser): + def __init__(self): + super(MLStripper, self).__init__() + self.reset() + self.strict = False + self.convert_charrefs = True + self.fed = [] + + def handle_data(self, d): + self.fed.append(d) + + def handle_entityref(self, name): + self.fed.append('&%s;' % name) + + def get_data(self): + return ''.join(self.fed) + + +def html_to_text(html): + s = MLStripper() + s.feed(html) + return s.get_data() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/views/__init__.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,28 @@ +from flask import render_template +from flask.views import View +from .menu import get_menu_context + + +class FoodTruckView(View): + template_name = 'index.html' + requires_menu = True + + def render_template(self, context): + if self.requires_menu: + context = with_menu_context() + return render_template(self.template_name, **context) + + def get_context(self): + return None + + def dispatch_request(self): + ctx = self.get_context() + return render_template(ctx) + + +def with_menu_context(context=None): + if context is None: + context = {} + context['menu'] = get_menu_context() + return context +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/views/create.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,91 @@ +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.sources.interfaces import IInteractiveSource +from piecrust.sources.base import MODE_CREATING +from piecrust.routing import create_route_metadata +from ..views import with_menu_context +from ..web import app + + +logger = logging.getLogger(__name__) + + +@app.route('/write/<source_name>', methods=['GET', 'POST']) +@login_required +def write_page(source_name): + site = g.site.piecrust_app + source = site.getSource(source_name) + if source is None: + abort(400) + if not isinstance(source, IInteractiveSource): + abort(400) + + 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.getRoute(source.name, fac.metadata, + skip_taxonomies=True) + 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)) + + 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 + data['source_name'] = source.name + data['url_postback'] = url_for('write_page', source_name=source.name) + data['fields'] = [] + for f in source.getInteractiveFields(): + data['fields'].append({ + 'name': f.name, + 'display_name': f.name, + 'type': f.field_type, + 'value': f.default_value}) + + with_menu_context(data) + return render_template('create_page.html', **data) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/views/dashboard.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,143 @@ +import os +import os.path +import logging +from flask import ( + g, request, + 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.uriutil import split_uri +from ..textutil import text_preview +from ..views import with_menu_context +from ..web import app, load_user, after_this_request + + +logger = logging.getLogger(__name__) + + +@app.route('/') +@login_required +def index(): + data = {} + data['sources'] = [] + site = g.site + fs_endpoints = {} + for source in site.piecrust_app.sources: + if source.is_theme_source: + continue + facs = source.getPageFactories() + src_data = { + 'name': source.name, + 'list_url': url_for('list_source', source_name=source.name), + 'page_count': len(facs)} + data['sources'].append(src_data) + + fe = getattr(source, 'fs_endpoint', None) + if fe: + fs_endpoints[fe] = source + + st = site.scm.getStatus() + data['new_pages'] = [] + for p in st.new_files: + pd = _getWipData(p, site, fs_endpoints) + if pd: + data['new_pages'].append(pd) + data['edited_pages'] = [] + for p in st.edited_files: + pd = _getWipData(p, site, fs_endpoints) + if pd: + data['edited_pages'].append(pd) + + data['site_name'] = site.name + data['site_title'] = site.piecrust_app.config.get('site/title', site.name) + 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') + + with_menu_context(data) + return render_template('dashboard.html', **data) + + +def _getWipData(path, site, fs_endpoints): + source = None + for endpoint, s in fs_endpoints.items(): + if path.startswith(endpoint): + source = s + break + if source is None: + return None + + fac = source.buildPageFactory(os.path.join(site.root_dir, path)) + route = site.piecrust_app.getRoute( + source.name, fac.metadata, skip_taxonomies=True) + if not route: + return None + + qp = QualifiedPage(fac.buildPage(), route, fac.metadata) + uri = qp.getUri() + _, slug = split_uri(site.piecrust_app, uri) + + with open(fac.path, 'r', encoding='utf8') as fp: + raw_text = fp.read() + + header, offset = parse_config_header(raw_text) + extract = text_preview(raw_text, offset=offset) + return { + 'title': qp.config.get('title'), + 'slug': slug, + 'url': url_for('edit_page', slug=slug), + 'text': extract + } + + +@login_required +@app.route('/switch_site', methods=['POST']) +def switch_site(): + site_name = request.form.get('site_name') + if not site_name: + return redirect(url_for('index')) + + @after_this_request + def _save_site(resp): + resp.set_cookie('foodtruck_site_name', site_name) + + return redirect(url_for('index')) + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + data = {} + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + remember = request.form.get('remember') + + user = load_user(username) + if user is not None and app.bcrypt: + if app.bcrypt.check_password_hash(user.password, password): + login_user(user, remember=bool(remember)) + return redirect(url_for('index')) + data['message'] = ( + "User '%s' doesn't exist or password is incorrect." % + username) + + return render_template('login.html', **data) + + +@app.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('index')) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/views/edit.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,75 @@ +import os.path +import logging +from flask import ( + g, request, abort, render_template, url_for, flash) +from flask.ext.login import login_required +from piecrust.rendering import ( + PageRenderingContext, render_page) +from piecrust.serving.util import get_requested_page +from ..views import with_menu_context +from ..web import app + + +logger = logging.getLogger(__name__) + + +@app.route('/edit/', defaults={'slug': ''}, methods=['GET', 'POST']) +@app.route('/edit/<path:slug>', methods=['GET', 'POST']) +@login_required +def edit_page(slug): + 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 + 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_preview' in request.form or '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') as fp: + fp.write(page_text) + flash("%s was saved." % os.path.relpath( + page.path, site_app.root_dir)) + + 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) + site.scm.commit([page.path], message) + + if 'do_preview' in request.form: + return _preview_page(page) + + if 'do_save' in request.form or 'do_save_and_commit' in request.form: + return _edit_page_form(page) + + abort(400) + + return _edit_page_form(page) + + +def _preview_page(page): + render_ctx = PageRenderingContext(page, force_render=True) + rp = render_page(render_ctx) + return rp.content + + +def _edit_page_form(page): + data = {} + data['is_new_page'] = False + data['url_cancel'] = url_for('list_source', source_name=page.source.name) + with open(page.path, 'r', encoding='utf8') as fp: + data['page_text'] = fp.read() + data['is_dos_nl'] = "1" if '\r\n' in data['page_text'] else "0" + + with_menu_context(data) + return render_template('edit_page.html', **data) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/views/menu.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,68 @@ +from flask import g, request, url_for +from flask.ext.login import current_user + + +def get_menu_context(): + entries = [] + entries.append({ + 'url': '/', + 'title': "Dashboard", + 'icon': 'speedometer'}) + + site = g.site.piecrust_app + for s in site.sources: + if s.is_theme_source: + 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' + + url_write = url_for('write_page', source_name=s.name) + url_listall = url_for('list_source', source_name=s.name) + + ctx = { + 'url': url_listall, + 'title': s.name, + 'icon': source_icon, + 'entries': [ + {'url': url_listall, 'title': "List All"}, + {'url': url_write, 'title': "Write New"} + ] + } + entries.append(ctx) + + entries.append({ + 'url': url_for('publish'), + 'title': "Publish", + 'icon': 'upload'}) + + # entries.append({ + # 'url': url_for('settings'), + # 'title': "Settings", + # 'icon': 'gear-b'}) + + for e in entries: + needs_more_break = False + if 'entries' in e: + for e2 in e['entries']: + if e2['url'] == request.path: + e['open'] = True + e2['active'] = True + needs_more_break = True + break + if needs_more_break: + break + + if e['url'] == request.path: + e['active'] = True + break + + data = {'entries': entries, + 'user': current_user, + 'url_logout': url_for('logout')} + return data + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/views/preview.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,25 @@ +import os.path +from flask import g, make_response +from flask.ext.login import login_required +from piecrust import CACHE_DIR +from piecrust.serving.server import Server +from ..web import app + + +@app.route('/site/<sitename>/') +@login_required +def preview_site_root(sitename): + return preview_site(sitename, '/') + + +@app.route('/site/<sitename>/<path:url>') +@login_required +def preview_site(sitename, url): + root_dir = g.sites.get_root_dir(sitename) + sub_cache_dir = os.path.join(root_dir, CACHE_DIR, 'foodtruck') + server = Server(root_dir, sub_cache_dir=sub_cache_dir, + root_url='/site/%s/' % sitename, + debug=app.debug) + return make_response(server._run_request) + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/views/publish.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,59 @@ +import copy +import logging +from flask import request, g, url_for, render_template, Response +from flask.ext.login import login_required +from ..pubutil import PublishLogReader +from ..views import with_menu_context +from ..web import app + + +logger = logging.getLogger(__name__) + + +@app.route('/publish', methods=['GET', 'POST']) +@login_required +def publish(): + if request.method == 'POST': + target = request.form.get('target') + if not target: + raise Exception("No target specified.") + + g.site.publish(target) + + 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 " + "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['targets'] = [] + for tn in sorted(pub_cfg.keys()): + tc = pub_cfg[tn] + desc = None + if isinstance(tc, dict): + desc = tc.get('description') + data['targets'].append({ + 'name': tn, + 'description': desc + }) + + with_menu_context(data) + + return render_template('publish.html', **data) + + +@app.route('/publish-log') +@login_required +def stream_publish_log(): + pid_path = g.site.publish_pid_file + log_path = g.site.publish_log_file + rdr = PublishLogReader(pid_path, log_path) + + response = Response(rdr.run(), mimetype='text/event-stream') + response.headers['Cache-Control'] = 'no-cache' + return response +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/views/sources.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,57 @@ +from flask import g, abort, render_template, url_for +from flask.ext.login import login_required +from piecrust.data.paginator import Paginator +from ..textutil import text_preview, html_to_text +from ..views import with_menu_context +from ..web import app + + +@app.route('/list/<source_name>/', defaults={'page_num': 1}) +@app.route('/list/<source_name>/<int:page_num>') +@login_required +def list_source(source_name, page_num): + site = g.site.piecrust_app + source = site.getSource(source_name) + if source is None: + abort(400) + + i = 0 + data = {'title': "List %s" % source_name} + data['pages'] = [] + pgn = Paginator(None, source, page_num=page_num, items_per_page=20) + for p in pgn.items: + page_data = { + 'title': p['title'], + 'slug': p['slug'], + 'source': source_name, + 'url': url_for('edit_page', slug=p['slug']), + 'text': text_preview(html_to_text(p['content']), length=300)} + data['pages'].append(page_data) + + prev_page_url = None + if pgn.prev_page_number: + prev_page_url = url_for( + 'list_source', source_name=source_name, + page_num=pgn.prev_page_number) + next_page_url = None + if pgn.next_page_number: + next_page_url = url_for( + 'list_source', source_name=source_name, + page_num=pgn.next_page_number) + + page_urls = [] + for i in pgn.all_page_numbers(7): + url = None + if i != page_num: + url = url_for('list_source', source_name=source_name, page_num=i) + page_urls.append({'num': i, 'url': url}) + + data['pagination'] = { + 'prev_page': prev_page_url, + 'next_page': next_page_url, + 'nums': page_urls + } + + with_menu_context(data) + return render_template('list_source.html', **data) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/web.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,172 @@ +import os +import os.path +import logging +from flask import Flask, g, request, render_template +from .configuration import ( + FoodTruckConfigNotFoundError, get_foodtruck_config) +from .sites import FoodTruckSites, InvalidSiteError + + +logger = logging.getLogger(__name__) + +app = Flask(__name__) +app.config.from_object('foodtruck.settings') +app.config.from_envvar('FOODTRUCK_SETTINGS', silent=True) + +admin_root = app.config['FOODTRUCK_ROOT'] or os.getcwd() +config_path = os.path.join(admin_root, 'app.cfg') + +# If we're being run as the `chef admin run` command, from inside a PieCrust +# website, do a few things differently. +_procedural_config = None + +if (app.config['FOODTRUCK_CMDLINE_MODE'] and + os.path.isfile(os.path.join(admin_root, 'config.yml'))): + app.secret_key = os.urandom(22) + app.config['LOGIN_DISABLED'] = True + _procedural_config = { + 'sites': { + 'local': admin_root} + } + + +if os.path.isfile(config_path): + app.config.from_pyfile(config_path) + +if app.config['DEBUG']: + l = logging.getLogger() + l.setLevel(logging.DEBUG) + +logger.debug("Using FoodTruck admin root: %s" % admin_root) + + +def after_this_request(f): + if not hasattr(g, 'after_request_callbacks'): + g.after_request_callbacks = [] + g.after_request_callbacks.append(f) + return f + + +class LazySomething(object): + def __init__(self, factory): + self._factory = factory + self._something = None + + def __getattr__(self, name): + if self._something is not None: + return getattr(self._something, name) + + self._something = self._factory() + return getattr(self._something, name) + + +@app.before_request +def _setup_foodtruck_globals(): + def _get_config(): + return get_foodtruck_config(admin_root, _procedural_config) + + def _get_sites(): + names = g.config.get('sites') + if not names or not isinstance(names, dict): + raise InvalidSiteError( + "No sites are defined in the configuration file.") + + current = request.cookies.get('foodtruck_site_name') + if current is not None and current not in names: + current = None + if current is None: + current = next(iter(names.keys())) + s = FoodTruckSites(g.config, current) + return s + + def _get_current_site(): + return g.sites.get() + + g.config = LazySomething(_get_config) + g.sites = LazySomething(_get_sites) + g.site = LazySomething(_get_current_site) + + +@app.after_request +def _call_after_request_callbacks(response): + for callback in getattr(g, 'after_request_callbacks', ()): + callback(response) + return response + + +if not app.config['DEBUG']: + logger.debug("Registering exception handlers.") + + @app.errorhandler(FoodTruckConfigNotFoundError) + def _on_config_missing(ex): + return render_template('install.html') + + @app.errorhandler(InvalidSiteError) + def _on_invalid_site(ex): + data = {'error': "The was an error with your configuration file: %s" % + str(ex)} + return render_template('error.html', **data) + + +@app.errorhandler +def _on_error(ex): + logging.exception(ex) + + +_missing_secret_key = False + +if not app.secret_key: + # If there's no secret key, create a temp one but mark the app as not + # correctly installed so it shows the installation information page. + app.secret_key = 'temp-key' + _missing_secret_key = True + + +from flask.ext.login import LoginManager, UserMixin + + +class User(UserMixin): + def __init__(self, uid, pwd): + self.id = uid + self.password = pwd + + +def load_user(user_id): + admin_id = g.config.get('security/username') + if admin_id == user_id: + admin_pwd = g.config.get('security/password') + return User(admin_id, admin_pwd) + return None + + +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'login' +login_manager.user_loader(load_user) + +if _missing_secret_key: + def _handler(): + raise FoodTruckConfigNotFoundError() + + logger.debug("No secret key found, disabling website.") + login_manager.unauthorized_handler(_handler) + login_manager.login_view = None + + +from foodtruck.bcryptfallback import Bcrypt +if (getattr(Bcrypt, 'is_fallback_bcrypt', None) is True and + not app.config['FOODTRUCK_CMDLINE_MODE']): + raise Exception( + "You're running FoodTruck outside of `chef`, and will need to " + "install Flask-Bcrypt to get more proper security.") +app.bcrypt = Bcrypt(app) + + +import foodtruck.views.create # NOQA +import foodtruck.views.dashboard # NOQA +import foodtruck.views.edit # NOQA +import foodtruck.views.menu # NOQA +import foodtruck.views.preview # NOQA +import foodtruck.views.publish # NOQA +import foodtruck.views.sources # NOQA +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/wsgiutil.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,23 @@ +import logging + + +logger = logging.getLogger() + + +def get_wsgi_app(admin_root=None, log_file=None, + max_log_bytes=4096, log_backup_count=0, + log_level=logging.INFO): + if log_file: + from logging.handlers import RotatingFileHandler + handler = RotatingFileHandler(log_file, maxBytes=max_log_bytes, + backupCount=log_backup_count) + handler.setLevel(log_level) + logging.getLogger().addHandler(handler) + + logger.debug("Creating WSGI application.") + if admin_root: + import foodtruck.settings + foodtruck.settings.FOODTRUCK_ROOT = admin_root + from foodtruck.web import app + return app +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/benchsite.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,227 @@ +import io +import os +import os.path +import string +import random +import datetime +import argparse + + +def generateWord(min_len=1, max_len=10): + length = random.randint(min_len, max_len) + word = ''.join(random.choice(string.ascii_letters) for _ in range(length)) + return word + + +def generateSentence(words): + return ' '.join([generateWord() for i in range(words)]) + + +def generateDate(): + year = random.choice(range(1995, 2015)) + month = random.choice(range(1, 13)) + day = random.choice(range(1, 29)) + hours = random.choice(range(0, 24)) + minutes = random.choice(range(0, 60)) + seconds = random.choice(range(0, 60)) + return datetime.datetime( + year, month, day, hours, minutes, seconds) + + +def generateTitleAndSlug(): + title = generateSentence(8) + slug = title.replace(' ', '-').lower() + slug = ''.join(c for c in slug if c.isalnum() or c == '-') + return title, slug + + +class BenchmarkSiteGenerator(object): + def __init__(self, out_dir): + self.out_dir = out_dir + self.all_tags = [] + + def generatePost(self): + post_info = {} + title, slug = generateTitleAndSlug() + post_info.update({ + 'title': title, + 'slug': slug}) + post_info['description'] = generateSentence(20) + post_info['tags'] = random.choice(self.all_tags) + post_info['datetime'] = generateDate() + + buf = io.StringIO() + with buf: + para_count = random.randint(5, 10) + for i in range(para_count): + buf.write(generateSentence(random.randint(50, 100))) + buf.write('\n\n') + post_info['text'] = buf.getvalue() + + self.writePost(post_info) + + def initialize(self): + pass + + def writePost(self, post_info): + raise NotImplementedError() + + +class PieCrustBechmarkSiteGenerator(BenchmarkSiteGenerator): + def initialize(self): + posts_dir = os.path.join(self.out_dir, 'posts') + if not os.path.isdir(posts_dir): + os.makedirs(posts_dir) + + def writePost(self, post_info): + out_dir = os.path.join(self.out_dir, 'posts') + slug = post_info['slug'] + dtstr = post_info['datetime'].strftime('%Y-%m-%d') + with open('%s/%s_%s.md' % (out_dir, dtstr, slug), 'w', + encoding='utf8') as f: + f.write('---\n') + f.write('title: %s\n' % post_info['title']) + f.write('description: %s\n' % post_info['description']) + f.write('tags: [%s]\n' % post_info['tags']) + f.write('---\n') + + para_count = random.randint(5, 10) + for i in range(para_count): + f.write(generateSentence(random.randint(50, 100))) + f.write('\n\n') + + +class OctopressBenchmarkSiteGenerator(BenchmarkSiteGenerator): + def initialize(self): + posts_dir = os.path.join(self.out_dir, 'source', '_posts') + if not os.path.isdir(posts_dir): + os.makedirs(posts_dir) + + def writePost(self, post_info): + out_dir = os.path.join(self.out_dir, 'source', '_posts') + slug = post_info['slug'] + dtstr = post_info['datetime'].strftime('%Y-%m-%d') + with open('%s/%s-%s.markdown' % (out_dir, dtstr, slug), 'w', + encoding='utf8') as f: + f.write('---\n') + f.write('layout: post\n') + f.write('title: %s\n' % post_info['title']) + f.write('date: %s 12:00\n' % dtstr) + f.write('comments: false\n') + f.write('categories: [%s]\n' % post_info['tags']) + f.write('---\n') + + para_count = random.randint(5, 10) + for i in range(para_count): + f.write(generateSentence(random.randint(50, 100))) + f.write('\n\n') + + +class MiddlemanBenchmarkSiteGenerator(BenchmarkSiteGenerator): + def initialize(self): + posts_dir = os.path.join(self.out_dir, 'source') + if not os.path.isdir(posts_dir): + os.makedirs(posts_dir) + + def writePost(self, post_info): + out_dir = os.path.join(self.out_dir, 'source') + slug = post_info['slug'] + dtstr = post_info['datetime'].strftime('%Y-%m-%d') + with open('%s/%s-%s.html.markdown' % (out_dir, dtstr, slug), 'w', + encoding='utf8') as f: + f.write('---\n') + f.write('title: %s\n' % post_info['title']) + f.write('date: %s\n' % post_info['datetime'].strftime('%Y/%m/%d')) + f.write('tags: %s\n' % post_info['tags']) + f.write('---\n') + + para_count = random.randint(5, 10) + for i in range(para_count): + f.write(generateSentence(random.randint(50, 100))) + f.write('\n\n') + + +class HugoBenchmarkSiteGenerator(BenchmarkSiteGenerator): + def initialize(self): + posts_dir = os.path.join(self.out_dir, 'content', 'post') + if not os.path.isdir(posts_dir): + os.makedirs(posts_dir) + + def writePost(self, post_info): + out_dir = os.path.join(self.out_dir, 'content', 'post') + dtstr = post_info['datetime'].strftime('%Y-%m-%d_%H-%M-%S') + post_path = os.path.join(out_dir, '%s.md' % dtstr) + with open(post_path, 'w', encoding='utf8') as f: + f.write('+++\n') + f.write('title = "%s"\n' % post_info['title']) + f.write('description = "%s"\n' % post_info['description']) + f.write('categories = [\n "%s"\n]\n' % post_info['tags']) + f.write('date = "%s"\n' % post_info['datetime'].strftime( + "%Y-%m-%d %H:%M:%S-00:00")) + f.write('slug ="%s"\n' % post_info['slug']) + f.write('+++\n') + f.write(post_info['text']) + + +generators = { + 'piecrust': PieCrustBechmarkSiteGenerator, + 'octopress': OctopressBenchmarkSiteGenerator, + 'middleman': MiddlemanBenchmarkSiteGenerator, + 'hugo': HugoBenchmarkSiteGenerator + } + + +def main(): + parser = argparse.ArgumentParser( + prog='generate_benchsite', + description=("Generates a benchmark website with placeholder " + "content suitable for testing.")) + parser.add_argument( + 'engine', + help="The engine to generate the site for.", + choices=list(generators.keys())) + parser.add_argument( + 'out_dir', + help="The target directory for the website.") + parser.add_argument( + '-c', '--post-count', + help="The number of posts to create.", + type=int, + default=100) + parser.add_argument( + '--tag-count', + help="The number of tags to use.", + type=int, + default=30) + + result = parser.parse_args() + generate(result.engine, result.out_dir, + post_count=result.post_count, + tag_count=result.tag_count) + + +def generate(engine, out_dir, post_count=100, tag_count=10): + print("Generating %d posts in %s..." % (post_count, out_dir)) + + if not os.path.exists(out_dir): + os.makedirs(out_dir) + + gen = generators[engine](out_dir) + gen.all_tags = [generateWord(3, 12) for _ in range(tag_count)] + gen.initialize() + + for i in range(post_count): + gen.generatePost() + + +if __name__ == '__main__': + main() +else: + from invoke import task + + @task + def genbenchsite(engine, out_dir, post_count=100, tag_count=10): + generate(engine, out_dir, + post_count=post_count, + tag_count=tag_count) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/changelog.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,183 @@ +import os +import os.path +import re +import time +import argparse +import subprocess + + +hg_log_template = ("{if(tags, '>>{tags};{date|shortdate}\n')}" + "{desc|firstline}\n\n") + +re_add_tag_changeset = re.compile('^Added tag [^\s]+ for changeset [\w\d]+$') +re_merge_pr_changeset = re.compile('^Merge pull request') +re_tag = re.compile('^\d+\.\d+\.\d+([ab]\d+)?(rc\d+)?$') +re_change = re.compile('^(\w+):') +re_clean_code_span = re.compile('([^\s])``([^\s]+)') + +category_commands = [ + 'chef', 'bake', 'find', 'help', 'import', 'init', 'paths', 'plugin', + 'plugins', 'prepare', 'purge', 'root', 'routes', 'serve', + 'showconfig', 'showrecord', 'sources', 'theme', 'themes', 'admin', + 'publish'] +category_core = [ + 'internal', 'bug', 'templating', 'formatting', 'performance', + 'data', 'config', 'rendering', 'render', 'debug', 'reporting', + 'linker', 'pagination', 'routing', 'caching', 'cli'] +category_project = ['build', 'cm', 'docs', 'tests', 'setup'] +categories = [ + ('commands', category_commands), + ('core', category_core), + ('project', category_project), + ('miscellaneous', None)] +category_names = list(map(lambda i: i[0], categories)) + + +def generate(out_file, last=None): + print("Generating %s" % out_file) + + if not os.path.exists('.hg'): + raise Exception("You must run this script from the root of a " + "Mercurial clone of the PieCrust repository.") + hglog = subprocess.check_output([ + 'hg', 'log', + '--rev', 'reverse(::master)', + '--template', hg_log_template]) + hglog = hglog.decode('utf8') + + templates = _get_templates() + + with open(out_file, 'w', encoding='utf8', newline='') as fp: + fp.write(templates['header']) + + skip = False + in_desc = False + current_version = 0 + current_version_info = None + current_changes = None + + if last: + current_version = 1 + cur_date = time.strftime('%Y-%m-%d') + current_version_info = last, cur_date + current_changes = {} + + for line in hglog.splitlines(): + if line == '': + skip = False + in_desc = False + continue + + if not in_desc and line.startswith('>>'): + tags, tag_date = line[2:].split(';') + if re_tag.match(tags): + if current_version > 0: + _write_version_changes( + templates, + current_version, current_version_info, + current_changes, fp) + + current_version += 1 + current_version_info = tags, tag_date + current_changes = {} + in_desc = True + else: + skip = True + continue + + if skip or current_version == 0: + continue + + if re_add_tag_changeset.match(line): + continue + if re_merge_pr_changeset.match(line): + continue + + m = re_change.match(line) + if m: + ch_type = m.group(1) + for cat_name, ch_types in categories: + if ch_types is None or ch_type in ch_types: + msgs = current_changes.setdefault(cat_name, []) + msgs.append(line) + break + else: + assert False, ("Change '%s' should have gone in the " + "misc. bucket." % line) + else: + msgs = current_changes.setdefault('miscellaneous', []) + msgs.append(line) + + if current_version > 0: + _write_version_changes( + templates, + current_version, current_version_info, + current_changes, fp) + + +def _write_version_changes(templates, version, version_info, changes, fp): + tokens = { + 'num': str(version), + 'version': version_info[0], + 'date': version_info[1]} + tpl = _multi_replace(templates['version_title'], tokens) + fp.write(tpl) + + for i, cat_name in enumerate(category_names): + msgs = changes.get(cat_name) + if not msgs: + continue + + tokens = { + 'sub_num': str(i), + 'category': cat_name.title()} + tpl = _multi_replace(templates['category_title'], tokens) + fp.write(tpl) + + for msg in msgs: + msg = msg.replace('`', '``').rstrip('\n') + msg = re_clean_code_span.sub(r'\1`` \2', msg) + fp.write('* ' + msg + '\n') + + +def _multi_replace(s, tokens): + for token in tokens: + s = s.replace('%%%s%%' % token, tokens[token]) + return s + + +def _get_templates(): + tpl_dir = os.path.join(os.path.dirname(__file__), 'changelog') + tpls = {} + for name in os.listdir(tpl_dir): + tpl = _get_template(os.path.join(tpl_dir, name)) + name_no_ext, _ = os.path.splitext(name) + tpls[name_no_ext] = tpl + return tpls + + +def _get_template(filename): + with open(filename, 'r', encoding='utf8', newline='') as fp: + return fp.read() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Generate CHANGELOG file.') + parser.add_argument( + 'out_file', + nargs='?', + default='CHANGELOG.rst', + help='The output file.') + parser.add_argument( + '--last', + help="The version for the last few untagged changes.") + args = parser.parse_args() + + generate(args.out_file, last=args.last) +else: + from invoke import task + + @task + def genchangelog(out_file='CHANGELOG.rst', last=None): + generate(out_file, last) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/changelog/category_title.rst Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,4 @@ + +1.%sub_num% %category% +---------------------- +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/changelog/header.rst Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,10 @@ + +######### +CHANGELOG +######### + +This is the changelog for PieCrust_. + +.. _PieCrust: http://bolt80.com/piecrust/ + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/changelog/version_title.rst Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,5 @@ + +================================== +%num%. PieCrust %version% (%date%) +================================== +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/documentation.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,50 @@ +import os +import os.path +from invoke import task, run + + +@task +def gendocs(tmp_dir=None, out_dir=None, root_url=None): + if not tmp_dir: + tmp_dir = '_docs-counter' + + if not os.path.isdir('venv'): + raise Exception( + "You need a virtual environment in the PieCrust repo.") + pyexe = os.path.join('venv', 'bin', 'python') + + print("Update node modules") + run("npm install") + + print("Update Bower packages") + run("bower update") + + print("Generate PieCrust version") + run(pyexe + ' setup.py version') + from piecrust.__version__ import APP_VERSION + version = APP_VERSION + + print("Baking documentation for version: %s" % version) + if root_url: + print("Using root URL: %s" % root_url) + args = [ + pyexe, 'chef.py', + '--root', 'docs', + '--config', 'dist'] + if root_url: + args += ['--config-set', 'site/root', root_url] + args += [ + 'bake', + '-o', tmp_dir + ] + run(' '.join(args)) + + if out_dir: + print("Synchronizing %s" % out_dir) + if not os.path.isdir(out_dir): + os.makedirs(out_dir) + + tmp_dir = tmp_dir.rstrip('/') + '/' + out_dir = out_dir.rstrip('/') + '/' + run('rsync -av --delete %s %s' % (tmp_dir, out_dir)) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/messages.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,11 @@ +import os +from invoke import task, run + + +@task +def genmessages(): + root_dir = 'garcon/messages' + out_dir = 'piecrust/resources/messages' + run('python chef.py --root %s bake -o %s' % (root_dir, out_dir)) + os.unlink('piecrust/resources/messages/index.html') +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/messages/config.yml Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,2 @@ +site: + title: PieCrust System Messages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/messages/pages/_index.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,12 @@ +--- +title: PieCrust System Messages +--- + +Here are the **PieCrust** system message pages: + +* [Requirements Not Met]({{ pcurl('requirements') }}) +* [Error]({{ pcurl('error') }}) +* [Not Found]({{ pcurl('error404') }}) +* [Critical Error]({{ pcurl('critical') }}) + +This very page you're reading, however, is only here for convenience.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/messages/pages/critical.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,6 @@ +--- +title: The Whole Kitchen Burned Down! +layout: error +--- +Something critically bad happened, and **PieCrust** needs to shut down. It's probably our fault. +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/messages/pages/error.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,5 @@ +--- +title: The Cake Just Burned! +layout: error +--- +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/messages/pages/error404.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,6 @@ +--- +title: Can't find the sugar! +layout: error +--- +It looks like the page you were trying to access does not exist around here. Try going somewhere else. +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/messages/templates/default.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,70 @@ +<!doctype html> +<html> +<head> + <title>{{ page.title }}</title> + <meta name="generator" content="PieCrust" /> + <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Lobster"> + <style> + body { + margin: 0; + padding: 1em; + background: #eee; + color: #000; + font-family: Georgia, serif; + } + h1 { + font-size: 4.5em; + font-family: Lobster, 'Trebuchet MS', Verdana, sans-serif; + text-align: center; + font-weight: bold; + margin-top: 0; + color: #333; + text-shadow: 0px 2px 5px rgba(0,0,0,0.3); + } + h2 { + font-size: 2.5em; + font-family: 'Lobster', 'Trebuchet MS', Verdana, sans-serif; + } + code { + background: #ddd; + padding: 0 0.2em; + } + #preamble { + font-size: 1.2em; + font-style: italic; + text-align: center; + margin-bottom: 0; + } + #container { + margin: 0 20%; + } + #content { + margin: 2em 1em; + } + .error-details { + color: #d11; + } + .note { + margin: 3em; + color: #888; + font-style: italic; + } + </style> +</head> +<body> + <div id="container"> + <div id="header"> + <p id="preamble">A Message From The Kitchen:</p> + <h1>{{ page.title }}</h1> + </div> + <hr /> + <div id="content"> + {% block content %} + {{ content|safe }} + {% endblock %} + </div> + <hr /> + {% block footer %}{% endblock %} + </div> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/messages/templates/error.html Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,27 @@ +{% extends "default.html" %} + +{% block content %} +{{content|safe}} + +{# The following is `raw` because we want it to be in the + produced page, so it can then be templated on the fly + with the error messages #} +{% raw %} +{% if details %} +<div class="error-details"> + <p>Error details:</p> + <ul> + {% for desc in details %} + <li>{{ desc }}</li> + {% endfor %} + </ul> +</div> +{% endif %} +{% endraw %} +{% endblock %} + +{% block footer %} +{% pcformat 'textile' %} +p(note). You're seeing this because something wrong happend. To see detailed errors with callstacks, run chef with the @--debug@ parameter, append @?!debug@ to the URL, or initialize the @PieCrust@ object with @{'debug': true}@. On the other hand, to see you custom error pages, set the @site/display_errors@ setting to @false@. +{% endpcformat %} +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/garcon/pypi.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,29 @@ +from invoke import task, run + + +@task +def makerelease(version, local_only=False): + if not version: + raise Exception("You must specify a version!") + + # FoodTruck assets. + print("Update node modules") + run("npm install") + print("Install Bower components") + run("bower install") + print("Generating FoodTruck assets") + run("gulp") + + # CHANGELOG.rst + run("invoke changelog --last %s" % version) + + if not local_only: + # Submit the CHANGELOG. + run('hg commit CHANGELOG.rst -m "cm: Regenerate the CHANGELOG."') + + # Tag in Mercurial, which will then be used for PyPi version. + run("hg tag %s" % version) + + # PyPi upload. + run("python setup.py sdist upload") +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gulpfile.js Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,81 @@ +'use strict'; + +var gulp = require('gulp'), + util = require('gulp-util'), + sass = require('gulp-sass'), + sourcemaps = require('gulp-sourcemaps'), + rename = require('gulp-rename'), + cssnano = require('gulp-cssnano'), + concat = require('gulp-concat'), + uglify = require('gulp-uglify'); + +// Stylesheets +gulp.task('sass', function() { + return gulp.src('foodtruck/assets/sass/**/*.scss') + //.pipe(sourcemaps.init()) + .pipe(sass({ + errLogToConsole: true, + outputStyle: 'compressed', + includePaths: [ + 'bower_components/bootstrap-sass/assets/stylesheets', + 'bower_components/Ionicons/scss']})) + .pipe(cssnano()) + //.pipe(sourcemaps.write()) + .pipe(rename({suffix: '.min'})) + .pipe(gulp.dest('foodtruck/static/css')); +}); +gulp.task('sass:watch', function() { + return gulp.watch('foodtruck/assets/sass/**/*.scss', ['sass']); +}); + +// Javascript +gulp.task('js', function() { + return gulp.src([ + 'bower_components/jquery/dist/jquery.js', + 'bower_components/bootstrap-sass/assets/javascripts/bootstrap/alert.js', + 'bower_components/bootstrap-sass/assets/javascripts/bootstrap/button.js', + 'bower_components/bootstrap-sass/assets/javascripts/bootstrap/collapse.js', + 'bower_components/bootstrap-sass/assets/javascripts/bootstrap/dropdown.js', + 'bower_components/bootstrap-sass/assets/javascripts/bootstrap/modal.js', + 'bower_components/bootstrap-sass/assets/javascripts/bootstrap/tooltip.js', + 'bower_components/bootstrap-sass/assets/javascripts/bootstrap/transition.js', + 'foodtruck/assets/js/**/*.js' + ]) + .pipe(sourcemaps.init()) + .pipe(concat('foodtruck.js')) + //.pipe(uglify()) + .pipe(sourcemaps.write()) + .pipe(rename({suffix: '.min'})) + .pipe(gulp.dest('foodtruck/static/js')); +}); +gulp.task('js:watch', function() { + return gulp.watch('foodtruck/assets/js/**/*.js', ['js']); +}); + +// Fonts/images +gulp.task('fonts', function() { + return gulp.src([ + 'bower_components/bootstrap-sass/assets/fonts/bootstrap/*', + 'bower_components/Ionicons/fonts/*' + ]) + .pipe(gulp.dest('foodtruck/static/fonts')); +}); + +gulp.task('images', function() { + return gulp.src([ + 'bower_components/bootstrap-sass/assets/images/*', + 'foodtruck/assets/img/*' + ]) + .pipe(gulp.dest('foodtruck/static/img')); +}); + +// Launch tasks +gulp.task('default', function() { + gulp.start(['sass', 'js', 'fonts', 'images']); +}); + +gulp.task('watch', function() { + gulp.start(['sass:watch', 'js:watch']); +}); + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/package.json Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,20 @@ +{ + "name": "PieCrust", + "version": "1.0.0", + "license": "Apache-2.0", + "repository": "http://bitbucket.org/ludovicchabant/piecrust2", + "devDependencies": { + "clean-css": "^3.4.9", + "gulp": "~3.9.0", + "gulp-concat": "~2.6.0", + "gulp-cssnano": "^2.1.1", + "gulp-less": "^3.0.5", + "gulp-rename": "~1.2.2", + "gulp-sass": "~2.1.0", + "gulp-sourcemaps": "~1.6.0", + "gulp-uglify": "^1.5.2", + "gulp-util": "~3.0.7", + "less": "^2.6.0", + "uglify-js": "^2.6.1" + } +}
--- a/piecrust/__init__.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/__init__.py Mon Feb 22 22:50:30 2016 -0800 @@ -17,6 +17,8 @@ PIECRUST_URL = 'http://bolt80.com/piecrust/' +CACHE_VERSION = 22 + try: from piecrust.__version__ import APP_VERSION except ImportError:
--- a/piecrust/app.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/app.py Mon Feb 22 22:50:30 2016 -0800 @@ -1,404 +1,26 @@ -import re -import json import time import os.path -import urllib.parse -import codecs import hashlib import logging -import collections -import yaml from werkzeug.utils import cached_property from piecrust import ( - APP_VERSION, RESOURCES_DIR, + RESOURCES_DIR, CACHE_DIR, TEMPLATES_DIR, ASSETS_DIR, THEME_DIR, - CONFIG_PATH, THEME_CONFIG_PATH, - DEFAULT_FORMAT, DEFAULT_TEMPLATE_ENGINE, DEFAULT_POSTS_FS, - DEFAULT_DATE_FORMAT, DEFAULT_THEME_SOURCE) -from piecrust.cache import ExtensibleCache, NullCache, NullExtensibleCache + CONFIG_PATH, THEME_CONFIG_PATH) +from piecrust.appconfig import PieCrustConfiguration +from piecrust.cache import ExtensibleCache, NullExtensibleCache from piecrust.plugins.base import PluginLoader from piecrust.environment import StandardEnvironment -from piecrust.configuration import ( - Configuration, ConfigurationError, ConfigurationLoader, - merge_dicts) +from piecrust.configuration import ConfigurationError from piecrust.routing import Route -from piecrust.sources.base import REALM_USER, REALM_THEME +from piecrust.sources.base import REALM_THEME from piecrust.taxonomies import Taxonomy logger = logging.getLogger(__name__) -CACHE_VERSION = 22 - - -class VariantNotFoundError(Exception): - def __init__(self, variant_path, message=None): - super(VariantNotFoundError, self).__init__( - message or ("No such configuration variant: %s" % variant_path)) - - -class PieCrustConfiguration(Configuration): - def __init__(self, paths=None, cache=None, values=None, validate=True): - super(PieCrustConfiguration, self).__init__(values, validate) - self.paths = paths - self.cache = cache or NullCache() - self.fixups = [] - - def applyVariant(self, variant_path, raise_if_not_found=True): - variant = self.get(variant_path) - if variant is None: - if raise_if_not_found: - raise VariantNotFoundError(variant_path) - return - if not isinstance(variant, dict): - raise VariantNotFoundError(variant_path, - "Configuration variant '%s' is not an array. " - "Check your configuration file." % variant_path) - self.merge(variant) - - def _load(self): - if self.paths is None: - self._values = self._validateAll({}) - return - - path_times = [os.path.getmtime(p) for p in self.paths] - cache_key_hash = hashlib.md5(("version=%s&cache=%d" % ( - APP_VERSION, CACHE_VERSION)).encode('utf8')) - for p in self.paths: - cache_key_hash.update(("&path=%s" % p).encode('utf8')) - cache_key = cache_key_hash.hexdigest() - - if self.cache.isValid('config.json', path_times): - logger.debug("Loading configuration from cache...") - config_text = self.cache.read('config.json') - self._values = json.loads(config_text, - object_pairs_hook=collections.OrderedDict) - - actual_cache_key = self._values.get('__cache_key') - if actual_cache_key == cache_key: - self._values['__cache_valid'] = True - return - logger.debug("Outdated cache key '%s' (expected '%s')." % ( - actual_cache_key, cache_key)) - - values = {} - logger.debug("Loading configuration from: %s" % self.paths) - for i, p in enumerate(self.paths): - with codecs.open(p, 'r', 'utf-8') as fp: - loaded_values = yaml.load(fp.read(), - Loader=ConfigurationLoader) - if loaded_values is None: - loaded_values = {} - for fixup in self.fixups: - fixup(i, loaded_values) - merge_dicts(values, loaded_values) - - for fixup in self.fixups: - fixup(len(self.paths), values) - - self._values = self._validateAll(values) - - logger.debug("Caching configuration...") - self._values['__cache_key'] = cache_key - config_text = json.dumps(self._values) - self.cache.write('config.json', config_text) - self._values['__cache_valid'] = False - - def _validateAll(self, values): - # Put all the defaults in the `site` section. - default_sitec = collections.OrderedDict({ - 'title': "Untitled PieCrust website", - 'root': '/', - 'default_format': DEFAULT_FORMAT, - 'default_template_engine': DEFAULT_TEMPLATE_ENGINE, - 'enable_gzip': True, - 'pretty_urls': False, - 'trailing_slash': False, - 'date_format': DEFAULT_DATE_FORMAT, - 'auto_formats': collections.OrderedDict([ - ('html', ''), - ('md', 'markdown'), - ('textile', 'textile')]), - 'default_auto_format': 'md', - 'pagination_suffix': '/%num%', - 'slugify_mode': 'encode', - 'plugins': None, - 'themes_sources': [DEFAULT_THEME_SOURCE], - 'cache_time': 28800, - 'enable_debug_info': True, - 'show_debug_info': False, - 'use_default_content': True - }) - sitec = values.get('site') - if sitec is None: - sitec = collections.OrderedDict() - for key, val in default_sitec.items(): - sitec.setdefault(key, val) - values['site'] = sitec - - # Add a section for our cached information. - cachec = collections.OrderedDict() - values['__cache'] = cachec - - # Make sure the site root starts and ends with a slash. - if not sitec['root'].startswith('/'): - raise ConfigurationError("The `site/root` setting must start " - "with a slash.") - sitec['root'] = urllib.parse.quote(sitec['root'].rstrip('/') + '/') - - # Cache auto-format regexes. - if not isinstance(sitec['auto_formats'], dict): - raise ConfigurationError("The 'site/auto_formats' setting must be " - "a dictionary.") - # Check that `.html` is in there. - sitec['auto_formats'].setdefault('html', sitec['default_format']) - cachec['auto_formats_re'] = r"\.(%s)$" % ( - '|'.join( - [re.escape(i) for i in - list(sitec['auto_formats'].keys())])) - if sitec['default_auto_format'] not in sitec['auto_formats']: - raise ConfigurationError("Default auto-format '%s' is not " - "declared." % - sitec['default_auto_format']) - - # Cache pagination suffix regex and format. - pgn_suffix = sitec['pagination_suffix'] - if len(pgn_suffix) == 0 or pgn_suffix[0] != '/': - raise ConfigurationError("The 'site/pagination_suffix' setting " - "must start with a slash.") - if '%num%' not in pgn_suffix: - raise ConfigurationError("The 'site/pagination_suffix' setting " - "must contain the '%num%' placeholder.") - - pgn_suffix_fmt = pgn_suffix.replace('%num%', '%(num)d') - cachec['pagination_suffix_format'] = pgn_suffix_fmt - - pgn_suffix_re = re.escape(pgn_suffix) - pgn_suffix_re = (pgn_suffix_re.replace("\\%num\\%", "(?P<num>\\d+)") + - '$') - cachec['pagination_suffix_re'] = pgn_suffix_re - - # Make sure theme sources is a list. - if not isinstance(sitec['themes_sources'], list): - sitec['themes_sources'] = [sitec['themes_sources']] - - # Figure out if we need to validate sources/routes, or auto-generate - # them from simple blog settings. - orig_sources = sitec.get('sources') - orig_routes = sitec.get('routes') - orig_taxonomies = sitec.get('taxonomies') - use_default_content = sitec.get('use_default_content') - if (orig_sources is None or orig_routes is None or - orig_taxonomies is None or use_default_content): - - # Setup defaults for various settings. - posts_fs = sitec.setdefault('posts_fs', DEFAULT_POSTS_FS) - blogsc = sitec.setdefault('blogs', ['posts']) - - g_page_layout = sitec.get('default_page_layout', 'default') - g_post_layout = sitec.get('default_post_layout', 'post') - g_post_url = sitec.get('post_url', '%year%/%month%/%day%/%slug%') - g_tag_url = sitec.get('tag_url', 'tag/%tag%') - g_category_url = sitec.get('category_url', '%category%') - g_posts_per_page = sitec.get('posts_per_page', 5) - g_posts_filters = sitec.get('posts_filters') - g_date_format = sitec.get('date_format', DEFAULT_DATE_FORMAT) - - # The normal pages and tags/categories. - sourcesc = collections.OrderedDict() - sourcesc['pages'] = { - 'type': 'default', - 'ignore_missing_dir': True, - 'data_endpoint': 'site.pages', - 'default_layout': g_page_layout, - 'item_name': 'page'} - sitec['sources'] = sourcesc - - routesc = [] - routesc.append({ - 'url': '/%path:slug%', - 'source': 'pages', - 'func': 'pcurl(slug)'}) - sitec['routes'] = routesc - - taxonomiesc = collections.OrderedDict() - taxonomiesc['tags'] = { - 'multiple': True, - 'term': 'tag'} - taxonomiesc['categories'] = { - 'term': 'category'} - sitec['taxonomies'] = taxonomiesc - - # Setup sources/routes/taxonomies for each blog. - for blog_name in blogsc: - blogc = values.get(blog_name, {}) - url_prefix = blog_name + '/' - fs_endpoint = 'posts/%s' % blog_name - data_endpoint = blog_name - item_name = '%s-post' % blog_name - items_per_page = blogc.get('posts_per_page', g_posts_per_page) - items_filters = blogc.get('posts_filters', g_posts_filters) - date_format = blogc.get('date_format', g_date_format) - if len(blogsc) == 1: - url_prefix = '' - fs_endpoint = 'posts' - data_endpoint = 'blog' - item_name = 'post' - sourcesc[blog_name] = { - 'type': 'posts/%s' % posts_fs, - 'fs_endpoint': fs_endpoint, - 'data_endpoint': data_endpoint, - 'ignore_missing_dir': True, - 'data_type': 'blog', - 'item_name': item_name, - 'items_per_page': items_per_page, - 'items_filters': items_filters, - 'date_format': date_format, - 'default_layout': g_post_layout} - tax_page_prefix = '' - if len(blogsc) > 1: - tax_page_prefix = blog_name + '/' - sourcesc[blog_name]['taxonomy_pages'] = { - 'tags': ('pages:%s_tag.%%ext%%;' - 'theme_pages:_tag.%%ext%%' % - tax_page_prefix), - 'categories': ('pages:%s_category.%%ext%%;' - 'theme_pages:_category.%%ext%%' % - tax_page_prefix)} - - post_url = blogc.get('post_url', url_prefix + g_post_url) - post_url = '/' + post_url.lstrip('/') - tag_url = blogc.get('tag_url', url_prefix + g_tag_url) - tag_url = '/' + tag_url.lstrip('/') - category_url = blogc.get('category_url', url_prefix + g_category_url) - category_url = '/' + category_url.lstrip('/') - routesc.append({'url': post_url, 'source': blog_name, - 'func': 'pcposturl(year,month,day,slug)'}) - routesc.append({'url': tag_url, 'source': blog_name, - 'taxonomy': 'tags', - 'func': 'pctagurl(tag)'}) - routesc.append({'url': category_url, 'source': blog_name, - 'taxonomy': 'categories', - 'func': 'pccaturl(category)'}) - - # If the user defined some additional sources/routes/taxonomies, - # add them to the default ones. For routes, the order matters, - # though, so we make sure to add the user routes at the front - # of the list so they're evaluated first. - if orig_sources: - sourcesc.update(orig_sources) - sitec['sources'] = sourcesc - if orig_routes: - routesc = orig_routes + routesc - sitec['routes'] = routesc - if orig_taxonomies: - taxonomiesc.update(orig_taxonomies) - sitec['taxonomies'] = taxonomiesc - - # Validate sources/routes. - sourcesc = sitec.get('sources') - routesc = sitec.get('routes') - if not sourcesc: - raise ConfigurationError("There are no sources defined.") - if not routesc: - raise ConfigurationError("There are no routes defined.") - if not isinstance(sourcesc, dict): - raise ConfigurationError("The 'site/sources' setting must be a " - "dictionary.") - if not isinstance(routesc, list): - raise ConfigurationError("The 'site/routes' setting must be a " - "list.") - - # Add the theme page source if no sources were defined in the theme - # configuration itself. - has_any_theme_source = False - for sn, sc in sourcesc.items(): - if sc.get('realm') == REALM_THEME: - has_any_theme_source = True - break - if not has_any_theme_source: - sitec['sources']['theme_pages'] = { - 'theme_source': True, - 'fs_endpoint': 'pages', - 'data_endpoint': 'site/pages', - 'item_name': 'page', - 'realm': REALM_THEME} - sitec['routes'].append({ - 'url': '/%path:slug%', - 'source': 'theme_pages', - 'func': 'pcurl(slug)'}) - - # Sources have the `default` scanner by default, duh. Also, a bunch - # of other default values for other configuration stuff. - for sn, sc in sourcesc.items(): - if not isinstance(sc, dict): - raise ConfigurationError("All sources in 'site/sources' must " - "be dictionaries.") - sc.setdefault('type', 'default') - sc.setdefault('fs_endpoint', sn) - sc.setdefault('ignore_missing_dir', False) - sc.setdefault('data_endpoint', sn) - sc.setdefault('data_type', 'iterator') - sc.setdefault('item_name', sn) - sc.setdefault('items_per_page', 5) - sc.setdefault('date_format', DEFAULT_DATE_FORMAT) - sc.setdefault('realm', REALM_USER) - - # Check routes are referencing correct routes, have default - # values, etc. - for rc in routesc: - if not isinstance(rc, dict): - raise ConfigurationError("All routes in 'site/routes' must be " - "dictionaries.") - rc_url = rc.get('url') - if not rc_url: - raise ConfigurationError("All routes in 'site/routes' must " - "have an 'url'.") - if rc_url[0] != '/': - raise ConfigurationError("Route URLs must start with '/'.") - if rc.get('source') is None: - raise ConfigurationError("Routes must specify a source.") - if rc['source'] not in list(sourcesc.keys()): - raise ConfigurationError("Route is referencing unknown " - "source: %s" % rc['source']) - rc.setdefault('taxonomy', None) - rc.setdefault('page_suffix', '/%num%') - - # Validate taxonomies. - sitec.setdefault('taxonomies', {}) - taxonomiesc = sitec.get('taxonomies') - for tn, tc in taxonomiesc.items(): - tc.setdefault('multiple', False) - tc.setdefault('term', tn) - tc.setdefault('page', '_%s.%%ext%%' % tc['term']) - - # Validate endpoints, and make sure the theme has a default source. - reserved_endpoints = set(['piecrust', 'site', 'page', 'route', - 'assets', 'pagination', 'siblings', - 'family']) - for name, src in sitec['sources'].items(): - endpoint = src['data_endpoint'] - if endpoint in reserved_endpoints: - raise ConfigurationError( - "Source '%s' is using a reserved endpoint name: %s" % - (name, endpoint)) - - # Make sure the `plugins` setting is a list. - user_plugins = sitec.get('plugins') - if user_plugins: - if isinstance(user_plugins, str): - sitec['plugins'] = user_plugins.split(',') - elif not isinstance(user_plugins, list): - raise ConfigurationError( - "The 'site/plugins' setting must be an array, or a " - "comma-separated list.") - - # Done validating! - return values - - class PieCrust(object): def __init__(self, root_dir, cache=True, debug=False, theme_site=False, env=None): @@ -420,6 +42,11 @@ self.env.registerTimer('SiteConfigLoad') self.env.registerTimer('PageLoad') self.env.registerTimer("PageDataBuild") + self.env.registerTimer("BuildRenderData") + self.env.registerTimer("PageRender") + self.env.registerTimer("PageRenderSegments") + self.env.registerTimer("PageRenderLayout") + self.env.registerTimer("PageSerialize") @cached_property def config(self): @@ -454,8 +81,8 @@ @cached_property def assets_dirs(self): - assets_dirs = self._get_configurable_dirs(ASSETS_DIR, - 'site/assets_dirs') + assets_dirs = self._get_configurable_dirs( + ASSETS_DIR, 'site/assets_dirs') # Also add the theme directory, if any. if self.theme_dir: @@ -467,8 +94,8 @@ @cached_property def templates_dirs(self): - templates_dirs = self._get_configurable_dirs(TEMPLATES_DIR, - 'site/templates_dirs') + templates_dirs = self._get_configurable_dirs( + TEMPLATES_DIR, 'site/templates_dirs') # Also, add the theme directory, if any. if self.theme_dir: @@ -505,7 +132,8 @@ for n, s in self.config.get('site/sources').items(): cls = defs.get(s['type']) if cls is None: - raise ConfigurationError("No such page source type: %s" % s['type']) + raise ConfigurationError("No such page source type: %s" % + s['type']) src = cls(self, n, s) sources.append(src) return sources @@ -548,7 +176,8 @@ def getTaxonomyRoute(self, tax_name, source_name): for route in self.routes: - if route.taxonomy_name == tax_name and route.source_name == source_name: + if (route.taxonomy_name == tax_name and + route.source_name == source_name): return route return None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/appconfig.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,514 @@ +import re +import os.path +import copy +import json +import urllib +import logging +import hashlib +import collections +import yaml +from piecrust import ( + APP_VERSION, CACHE_VERSION, + DEFAULT_FORMAT, DEFAULT_TEMPLATE_ENGINE, DEFAULT_POSTS_FS, + DEFAULT_DATE_FORMAT, DEFAULT_THEME_SOURCE) +from piecrust.cache import NullCache +from piecrust.configuration import ( + Configuration, ConfigurationError, ConfigurationLoader, + merge_dicts, visit_dict) +from piecrust.sources.base import REALM_USER, REALM_THEME + + +logger = logging.getLogger(__name__) + + +class VariantNotFoundError(Exception): + def __init__(self, variant_path, message=None): + super(VariantNotFoundError, self).__init__( + message or ("No such configuration variant: %s" % + variant_path)) + + +class PieCrustConfiguration(Configuration): + def __init__(self, paths=None, cache=None, values=None, validate=True): + super(PieCrustConfiguration, self).__init__(values, validate) + self.paths = paths + self.cache = cache or NullCache() + self.fixups = [] + + def applyVariant(self, variant_path, raise_if_not_found=True): + variant = self.get(variant_path) + if variant is None: + if raise_if_not_found: + raise VariantNotFoundError(variant_path) + return + if not isinstance(variant, dict): + raise VariantNotFoundError( + variant_path, + "Configuration variant '%s' is not an array. " + "Check your configuration file." % variant_path) + self.merge(variant) + + def _load(self): + if self.paths is None: + self._values = self._validateAll({}) + return + + path_times = [os.path.getmtime(p) for p in self.paths] + + cache_key_hash = hashlib.md5( + ("version=%s&cache=%d" % ( + APP_VERSION, CACHE_VERSION)).encode('utf8')) + for p in self.paths: + cache_key_hash.update(("&path=%s" % p).encode('utf8')) + cache_key = cache_key_hash.hexdigest() + + if self.cache.isValid('config.json', path_times): + logger.debug("Loading configuration from cache...") + config_text = self.cache.read('config.json') + self._values = json.loads( + config_text, + object_pairs_hook=collections.OrderedDict) + + actual_cache_key = self._values.get('__cache_key') + if actual_cache_key == cache_key: + self._values['__cache_valid'] = True + return + logger.debug("Outdated cache key '%s' (expected '%s')." % ( + actual_cache_key, cache_key)) + + logger.debug("Loading configuration from: %s" % self.paths) + values = {} + try: + for i, p in enumerate(self.paths): + with open(p, 'r', encoding='utf-8') as fp: + loaded_values = yaml.load( + fp.read(), + Loader=ConfigurationLoader) + if loaded_values is None: + loaded_values = {} + for fixup in self.fixups: + fixup(i, loaded_values) + merge_dicts(values, loaded_values) + + for fixup in self.fixups: + fixup(len(self.paths), values) + + self._values = self._validateAll(values) + except Exception as ex: + raise Exception("Error loading configuration from: %s" % + ', '.join(self.paths)) from ex + + logger.debug("Caching configuration...") + self._values['__cache_key'] = cache_key + config_text = json.dumps(self._values) + self.cache.write('config.json', config_text) + + self._values['__cache_valid'] = False + + def _validateAll(self, values): + if values is None: + values = {} + + # Add the loaded values to the default configuration. + values = merge_dicts(copy.deepcopy(default_configuration), values) + + # Figure out if we need to generate the configuration for the + # default content model. + sitec = values.setdefault('site', {}) + if ( + ('sources' not in sitec and + 'routes' not in sitec and + 'taxonomies' not in sitec) or + sitec.get('use_default_content')): + logger.debug("Generating default content model...") + values = self._generateDefaultContentModel(values) + + # Add a section for our cached information. + cachec = collections.OrderedDict() + values['__cache'] = cachec + cache_writer = _ConfigCacheWriter(cachec) + globs = globals() + + def _visitor(path, val, parent_val, parent_key): + callback_name = '_validate_' + path.replace('/', '_') + callback = globs.get(callback_name) + if callback: + try: + val2 = callback(val, values, cache_writer) + except Exception as ex: + raise Exception("Error raised in validator '%s'." % + callback_name) from ex + if val2 is None: + raise Exception("Validator '%s' isn't returning a " + "coerced value." % callback_name) + parent_val[parent_key] = val2 + + visit_dict(values, _visitor) + + return values + + def _generateDefaultContentModel(self, values): + dcmcopy = copy.deepcopy(default_content_model_base) + values = merge_dicts(dcmcopy, values) + + blogsc = values['site'].get('blogs') + if blogsc is None: + blogsc = ['posts'] + values['site']['blogs'] = blogsc + + is_only_blog = (len(blogsc) == 1) + for blog_name in blogsc: + blog_cfg = get_default_content_model_for_blog( + blog_name, is_only_blog, values) + values = merge_dicts(blog_cfg, values) + + dcm = get_default_content_model(values) + values = merge_dicts(dcm, values) + + return values + + +class _ConfigCacheWriter(object): + def __init__(self, cache_dict): + self._cache_dict = cache_dict + + def write(self, name, val): + logger.debug("Caching configuration item '%s' = %s" % (name, val)) + self._cache_dict[name] = val + + +default_configuration = collections.OrderedDict({ + 'site': collections.OrderedDict({ + 'title': "Untitled PieCrust website", + 'root': '/', + 'default_format': DEFAULT_FORMAT, + 'default_template_engine': DEFAULT_TEMPLATE_ENGINE, + 'enable_gzip': True, + 'pretty_urls': False, + 'trailing_slash': False, + 'date_format': DEFAULT_DATE_FORMAT, + 'auto_formats': collections.OrderedDict([ + ('html', ''), + ('md', 'markdown'), + ('textile', 'textile')]), + 'default_auto_format': 'md', + 'default_pagination_source': None, + 'pagination_suffix': '/%num%', + 'slugify_mode': 'encode', + 'themes_sources': [DEFAULT_THEME_SOURCE], + 'cache_time': 28800, + 'enable_debug_info': True, + 'show_debug_info': False, + 'use_default_content': True + }), + 'baker': collections.OrderedDict({ + 'no_bake_setting': 'draft', + 'workers': None, + 'batch_size': None + }) + }) + + +default_content_model_base = collections.OrderedDict({ + 'site': collections.OrderedDict({ + 'posts_fs': DEFAULT_POSTS_FS, + 'date_format': DEFAULT_DATE_FORMAT, + 'default_page_layout': 'default', + 'default_post_layout': 'post', + 'post_url': '%year%/%month%/%day%/%slug%', + 'tag_url': 'tag/%tag%', + 'category_url': '%category%', + 'posts_per_page': 5 + }) + }) + + +def get_default_content_model(values): + default_layout = values['site']['default_page_layout'] + return collections.OrderedDict({ + 'site': collections.OrderedDict({ + 'sources': collections.OrderedDict({ + 'pages': { + 'type': 'default', + 'ignore_missing_dir': True, + 'data_endpoint': 'site.pages', + 'default_layout': default_layout, + 'item_name': 'page' + } + }), + 'routes': [ + { + 'url': '/%path:slug%', + 'source': 'pages', + 'func': 'pcurl(slug)' + } + ], + 'taxonomies': collections.OrderedDict({ + 'tags': { + 'multiple': True, + 'term': 'tag' + }, + 'categories': { + 'term': 'category' + } + }) + }) + }) + + +def get_default_content_model_for_blog(blog_name, is_only_blog, values): + posts_fs = values['site']['posts_fs'] + blog_cfg = values.get(blog_name, {}) + + if is_only_blog: + url_prefix = '' + tax_page_prefix = '' + fs_endpoint = 'posts' + data_endpoint = 'blog' + item_name = 'post' + else: + url_prefix = blog_name + '/' + tax_page_prefix = blog_name + '/' + fs_endpoint = 'posts/%s' % blog_name + data_endpoint = blog_name + item_name = '%s-post' % blog_name + + items_per_page = blog_cfg.get( + 'posts_per_page', values['site']['posts_per_page']) + date_format = blog_cfg.get( + 'date_format', values['site']['date_format']) + default_layout = blog_cfg.get( + 'default_layout', values['site']['default_post_layout']) + + post_url = '/' + blog_cfg.get( + 'post_url', + url_prefix + values['site']['post_url']).lstrip('/') + tag_url = '/' + blog_cfg.get( + 'tag_url', + url_prefix + values['site']['tag_url']).lstrip('/') + category_url = '/' + blog_cfg.get( + 'category_url', + url_prefix + values['site']['category_url']).lstrip('/') + + return collections.OrderedDict({ + 'site': collections.OrderedDict({ + 'sources': collections.OrderedDict({ + blog_name: collections.OrderedDict({ + 'type': 'posts/%s' % posts_fs, + 'fs_endpoint': fs_endpoint, + 'data_endpoint': data_endpoint, + 'item_name': item_name, + 'ignore_missing_dir': True, + 'data_type': 'blog', + 'items_per_page': items_per_page, + 'date_format': date_format, + 'default_layout': default_layout, + 'taxonomy_pages': collections.OrderedDict({ + 'tags': ('pages:%s_tag.%%ext%%;' + 'theme_pages:_tag.%%ext%%' % + tax_page_prefix), + 'categories': ('pages:%s_category.%%ext%%;' + 'theme_pages:_category.%%ext%%' % + tax_page_prefix) + }) + }) + }), + 'routes': [ + { + 'url': post_url, + 'source': blog_name, + 'func': 'pcposturl(year,month,day,slug)' + }, + { + 'url': tag_url, + 'source': blog_name, + 'taxonomy': 'tags', + 'func': 'pctagurl(tag)' + }, + { + 'url': category_url, + 'source': blog_name, + 'taxonomy': 'categories', + 'func': 'pccaturl(category)' + } + ] + }) + }) + + +# Configuration value validators. +# +# Make sure we have basic site stuff. +def _validate_site(v, values, cache): + sources = v.get('sources') + if not sources: + raise ConfigurationError("No sources were defined.") + routes = v.get('routes') + if not routes: + raise ConfigurationError("No routes were defined.") + taxonomies = v.get('taxonomies') + if taxonomies is None: + v['taxonomies'] = {} + return v + +# Make sure the site root starts and ends with a slash. +def _validate_site_root(v, values, cache): + if not v.startswith('/'): + raise ConfigurationError("The `site/root` setting must start " + "with a slash.") + root_url = urllib.parse.quote(v.rstrip('/') + '/') + return root_url + + +# Cache auto-format regexes, check that `.html` is in there. +def _validate_site_auto_formats(v, values, cache): + if not isinstance(v, dict): + raise ConfigurationError("The 'site/auto_formats' setting must be " + "a dictionary.") + + v.setdefault('html', values['site']['default_format']) + auto_formats_re = r"\.(%s)$" % ( + '|'.join( + [re.escape(i) for i in list(v.keys())])) + cache.write('auto_formats_re', auto_formats_re) + return v + + +# Check that the default auto-format is known. +def _validate_site_default_auto_format(v, values, cache): + if v not in values['site']['auto_formats']: + raise ConfigurationError( + "Default auto-format '%s' is not declared." % v) + return v + + +# Cache pagination suffix regex and format. +def _validate_site_pagination_suffix(v, values, cache): + if len(v) == 0 or v[0] != '/': + raise ConfigurationError("The 'site/pagination_suffix' setting " + "must start with a slash.") + if '%num%' not in v: + raise ConfigurationError("The 'site/pagination_suffix' setting " + "must contain the '%num%' placeholder.") + + pgn_suffix_fmt = v.replace('%num%', '%(num)d') + cache.write('pagination_suffix_format', pgn_suffix_fmt) + + pgn_suffix_re = re.escape(v) + pgn_suffix_re = (pgn_suffix_re.replace("\\%num\\%", "(?P<num>\\d+)") + + '$') + cache.write('pagination_suffix_re', pgn_suffix_re) + return v + + +# Make sure theme sources is a list. +def _validate_site_theme_sources(v, values, cache): + if not isinstance(v, list): + v = [v] + return v + + +def _validate_site_sources(v, values, cache): + # Basic checks. + if not v: + raise ConfigurationError("There are no sources defined.") + if not isinstance(v, dict): + raise ConfigurationError("The 'site/sources' setting must be a " + "dictionary.") + + # Add the theme page source if no sources were defined in the theme + # configuration itself. + has_any_theme_source = False + for sn, sc in v.items(): + if sc.get('realm') == REALM_THEME: + has_any_theme_source = True + break + if not has_any_theme_source: + v['theme_pages'] = { + 'theme_source': True, + 'fs_endpoint': 'pages', + 'data_endpoint': 'site/pages', + 'item_name': 'page', + 'realm': REALM_THEME} + values['site']['routes'].append({ + 'url': '/%path:slug%', + 'source': 'theme_pages', + 'func': 'pcurl(slug)'}) + + # Sources have the `default` scanner by default, duh. Also, a bunch + # of other default values for other configuration stuff. + reserved_endpoints = set(['piecrust', 'site', 'page', 'route', + 'assets', 'pagination', 'siblings', + 'family']) + for sn, sc in v.items(): + if not isinstance(sc, dict): + raise ConfigurationError("All sources in 'site/sources' must " + "be dictionaries.") + sc.setdefault('type', 'default') + sc.setdefault('fs_endpoint', sn) + sc.setdefault('ignore_missing_dir', False) + sc.setdefault('data_endpoint', sn) + sc.setdefault('data_type', 'iterator') + sc.setdefault('item_name', sn) + sc.setdefault('items_per_page', 5) + sc.setdefault('date_format', DEFAULT_DATE_FORMAT) + sc.setdefault('realm', REALM_USER) + + # Validate endpoints. + endpoint = sc['data_endpoint'] + if endpoint in reserved_endpoints: + raise ConfigurationError( + "Source '%s' is using a reserved endpoint name: %s" % + (sn, endpoint)) + + return v + + +def _validate_site_routes(v, values, cache): + if not v: + raise ConfigurationError("There are no routes defined.") + if not isinstance(v, list): + raise ConfigurationError("The 'site/routes' setting must be a " + "list.") + + # Check routes are referencing correct sources, have default + # values, etc. + for rc in v: + if not isinstance(rc, dict): + raise ConfigurationError("All routes in 'site/routes' must be " + "dictionaries.") + rc_url = rc.get('url') + if not rc_url: + raise ConfigurationError("All routes in 'site/routes' must " + "have an 'url'.") + if rc_url[0] != '/': + raise ConfigurationError("Route URLs must start with '/'.") + if rc.get('source') is None: + raise ConfigurationError("Routes must specify a source.") + if rc['source'] not in list(values['site']['sources'].keys()): + raise ConfigurationError("Route is referencing unknown " + "source: %s" % rc['source']) + rc.setdefault('taxonomy', None) + rc.setdefault('page_suffix', '/%num%') + + return v + + +def _validate_site_taxonomies(v, values, cache): + for tn, tc in v.items(): + tc.setdefault('multiple', False) + tc.setdefault('term', tn) + tc.setdefault('page', '_%s.%%ext%%' % tc['term']) + + return v + + +def _validate_site_plugins(v, values, cache): + if isinstance(v, str): + v = v.split(',') + elif not isinstance(v, list): + raise ConfigurationError( + "The 'site/plugins' setting must be an array, or a " + "comma-separated list.") + return v +
--- a/piecrust/baking/single.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/baking/single.py Mon Feb 22 22:50:30 2016 -0800 @@ -159,13 +159,15 @@ if tax_info: ctx.setTaxonomyFilter(tax_info.term) - rp = render_page(ctx) + with self.app.env.timerScope("PageRender"): + rp = render_page(ctx) - out_dir = os.path.dirname(out_path) - _ensure_dir_exists(out_dir) + with self.app.env.timerScope("PageSerialize"): + out_dir = os.path.dirname(out_path) + _ensure_dir_exists(out_dir) - with codecs.open(out_path, 'w', 'utf8') as fp: - fp.write(rp.content) + with open(out_path, 'w', encoding='utf8') as fp: + fp.write(rp.content) return rp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/commands/builtin/admin.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,109 @@ +import os +import logging +from piecrust import CACHE_DIR +from piecrust.commands.base import ChefCommand + + +logger = logging.getLogger(__name__) + + +class AdministrationPanelCommand(ChefCommand): + def __init__(self): + super(AdministrationPanelCommand, self).__init__() + self.name = 'admin' + self.description = "Manages the PieCrust administration panel." + self.requires_website = False + + def setupParser(self, parser, app): + subparsers = parser.add_subparsers() + + p = subparsers.add_parser( + 'init', + help="Creates a new administration panel website.") + p.set_defaults(sub_func=self._initFoodTruck) + + p = subparsers.add_parser( + 'genpass', + help=("Generates the hashed password for use as an " + "admin password")) + p.add_argument('password', help="The password to hash.") + p.set_defaults(sub_func=self._generatePassword) + + p = subparsers.add_parser( + 'run', + help="Runs the administrative panel website.") + p.add_argument( + '-p', '--port', + help="The port for the administrative panel website.", + default=8090) + p.add_argument( + '-a', '--address', + help="The host for the administrative panel website.", + default='localhost') + p.set_defaults(sub_func=self._runFoodTruck) + + def checkedRun(self, ctx): + if not hasattr(ctx.args, 'sub_func'): + return self._runFoodTruck(ctx) + return ctx.args.sub_func(ctx) + + def _runFoodTruck(self, ctx): + from piecrust.processing.pipeline import ProcessorPipeline + out_dir = os.path.join( + ctx.app.root_dir, CACHE_DIR, 'foodtruck', 'server') + proc = ProcessorPipeline(ctx.app, out_dir) + proc.run() + + from foodtruck import settings + settings.FOODTRUCK_CMDLINE_MODE = True + settings.FOODTRUCK_ROOT = ctx.app.root_dir + from foodtruck.main import run_foodtruck + run_foodtruck( + host=ctx.args.address, + port=ctx.args.port, + debug=ctx.args.debug) + + def _initFoodTruck(self, ctx): + import getpass + import bcrypt + + secret_key = os.urandom(22) + admin_username = input("Admin username (admin): ") or 'admin' + 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.info("You can later set an admin password by editing " + "the `foodtruck.yml` file and using the " + "`chef admin genpass` command.") + else: + binpw = admin_password.encode('utf8') + hashpw = bcrypt.hashpw(binpw, bcrypt.gensalt()).decode('utf8') + admin_password = hashpw + + ft_config = """ +security: + username: %(username)s + # You can generate another hashed password with `chef admin genpass`. + password: %(password)s +""" + ft_config = ft_config % { + 'username': admin_username, + 'password': admin_password + } + with open('foodtruck.yml', 'w', encoding='utf8') as fp: + 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 foodtruck import bcryptfallback as bcrypt + binpw = ctx.args.password.encode('utf8') + hashpw = bcrypt.hashpw(binpw, bcrypt.gensalt()).decode('utf8') + logger.info(hashpw) +
--- a/piecrust/commands/builtin/baking.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/commands/builtin/baking.py Mon Feb 22 22:50:30 2016 -0800 @@ -5,6 +5,7 @@ import fnmatch import datetime from colorama import Fore +from piecrust import CACHE_DIR from piecrust.baking.baker import Baker from piecrust.baking.records import ( BakeRecord, BakeRecordEntry, SubPageBakeInfo)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/commands/builtin/publishing.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,78 @@ +import logging +import urllib.parse +from piecrust.commands.base import ChefCommand +from piecrust.publishing.publisher import Publisher, find_publisher_name + + +logger = logging.getLogger(__name__) + + +class PublishCommand(ChefCommand): + """ Command for running publish targets for the current site. + """ + def __init__(self): + super(PublishCommand, self).__init__() + self.name = 'publish' + self.description = "Publishes you website to a specific target." + + def setupParser(self, parser, app): + parser.add_argument( + '-l', '--list', + action='store_true', + help="List available publish targets for the current site.") + parser.add_argument( + '--log-publisher', + metavar='LOG_FILE', + help="Log the publisher's output to a given file.") + parser.add_argument( + '--preview', + action='store_true', + help="Only preview what the publisher would do.") + parser.add_argument( + 'target', + nargs='?', + default='default', + help="The publish target to use.") + + def run(self, ctx): + if ctx.args.list: + pub_cfg = ctx.app.config.get('publish') + if not pub_cfg: + logger.info("No available publish targets.") + return + + for name, cfg in pub_cfg.items(): + if isinstance(cfg, dict): + pub_type = cfg.get('type') + if pub_type: + desc = cfg.get('description') + bake_first = cfg.get('bake', True) + msg = '%s (%s)' % (name, pub_type) + if not bake_first: + msg += ' (no local baking)' + if desc: + msg += ': ' + desc + logger.info(msg) + else: + logger.error( + "%s (unknown type '%s')" % (name, pub_type)) + elif isinstance(cfg, str): + comps = urllib.parse.urlparse(str(cfg)) + pub_name = find_publisher_name(ctx.app, comps.scheme) + if pub_name: + logger.info("%s (%s)" % (name, pub_name)) + else: + logger.error( + "%s (unknown scheme '%s')" % + (name, comps.scheme)) + else: + logger.error( + "%s (incorrect configuration)" % name) + return + + pub = Publisher(ctx.app) + pub.run( + ctx.args.target, + preview=ctx.args.preview, + log_file=ctx.args.log_publisher) +
--- a/piecrust/configuration.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/configuration.py Mon Feb 22 22:50:30 2016 -0800 @@ -1,5 +1,6 @@ import re import logging +import collections import collections.abc import yaml from yaml.constructor import ConstructorError @@ -69,7 +70,7 @@ def setAll(self, values, validate=False): if validate: - self._validateAll(values) + values = self._validateAll(values) self._values = values def getAll(self): @@ -129,11 +130,10 @@ def merge_dicts(source, merging, validator=None, *args): - if validator is None: - validator = lambda k, v: v _recurse_merge_dicts(source, merging, None, validator) for other in args: _recurse_merge_dicts(source, other, None, validator) + return source def _recurse_merge_dicts(local_cur, incoming_cur, parent_path, validator): @@ -149,9 +149,28 @@ elif isinstance(v, list) and isinstance(local_v, list): local_cur[k] = v + local_v else: - local_cur[k] = validator(key_path, v) + if validator is not None: + v = validator(key_path, v) + local_cur[k] = v else: - local_cur[k] = validator(key_path, v) + if validator is not None: + v = validator(key_path, v) + local_cur[k] = v + + +def visit_dict(subject, visitor): + _recurse_visit_dict(subject, None, visitor) + + +def _recurse_visit_dict(cur, parent_path, visitor): + for k, v in cur.items(): + key_path = k + if parent_path is not None: + key_path = parent_path + '/' + k + + visitor(key_path, v, cur, k) + if isinstance(v, dict): + _recurse_visit_dict(v, key_path, visitor) header_regex = re.compile(
--- a/piecrust/data/debug.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/data/debug.py Mon Feb 22 22:50:30 2016 -0800 @@ -309,9 +309,11 @@ value = None render_name = name should_call = name in invoke_attrs + is_redirect = False if name in redirects: name = redirects[name] + is_redirect = True query_instance = False try: @@ -329,7 +331,10 @@ argcount = attr_func.__code__.co_argcount var_names = attr_func.__code__.co_varnames if argcount == 1 and should_call: - render_name += '()' + if not is_redirect: + # Most of the time, redirects are for making a + # property render better. So don't add parenthesis. + render_name += '()' value = attr_func() else: if should_call:
--- a/piecrust/data/iterators.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/data/iterators.py Mon Feb 22 22:50:30 2016 -0800 @@ -1,5 +1,5 @@ import logging -from piecrust.data.filters import PaginationFilter +from piecrust.data.filters import PaginationFilter, IsFilterClause, NotClause from piecrust.environment import AbortedSourceUseError from piecrust.events import Event from piecrust.sources.base import PageSource @@ -132,6 +132,20 @@ if src_it is not None: self._pages = src_it + # If we're currently baking, apply the default baker filter + # to exclude things like draft posts. + if (isinstance(source, PageSource) and + source.app.config.get('baker/is_baking')): + setting_name = source.app.config.get('baker/no_bake_setting', + 'draft') + accessor = self._getSettingAccessor() + draft_filter = PaginationFilter(accessor) + draft_filter.root_clause = NotClause() + draft_filter.root_clause.addClause( + IsFilterClause(setting_name, True)) + self._simpleNonSortedWrap( + PaginationFilterIterator, draft_filter) + # Apply any filter first, before we start sorting or slicing. if pagination_filter is not None: self._simpleNonSortedWrap(PaginationFilterIterator,
--- a/piecrust/data/linker.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/data/linker.py Mon Feb 22 22:50:30 2016 -0800 @@ -15,7 +15,13 @@ """ debug_render = ['parent', 'ancestors', 'siblings', 'children', 'root', 'forpath'] - debug_render_invoke = ['parent', 'siblings', 'children'] + debug_render_invoke = ['parent', 'ancestors', 'siblings', 'children', + 'root'] + debug_render_redirect = { + 'ancestors': '_debugRenderAncestors', + 'siblings': '_debugRenderSiblings', + 'children': '_debugRenderChildren', + 'root': '_debugRenderRoot'} def __init__(self, source, page_path): self._source = source @@ -84,6 +90,21 @@ self._linker = Linker(self._source, dir_path, root_page_path=self._root_page_path) + def _debugRenderAncestors(self): + return [i.name for i in self.ancestors] + + def _debugRenderSiblings(self): + return [i.name for i in self.siblings] + + def _debugRenderChildren(self): + return [i.name for i in self.children] + + def _debugRenderRoot(self): + r = self.root + if r is not None: + return r.name + return None + class LinkedPageData(PaginationData): """ Class whose instances get returned when iterating on a `Linker`
--- a/piecrust/data/paginator.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/data/paginator.py Mon Feb 22 22:50:30 2016 -0800 @@ -208,7 +208,7 @@ if self._pgn_filter is not None: f.addClause(self._pgn_filter.root_clause) - if isinstance(self._source, IPaginationSource): + if self._parent_page and isinstance(self._source, IPaginationSource): sf = self._source.getPaginationFilter(self._parent_page) if sf is not None: f.addClause(sf.root_clause)
--- a/piecrust/main.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/main.py Mon Feb 22 22:50:30 2016 -0800 @@ -1,7 +1,8 @@ +import os +import os.path import io import sys import time -import os.path import logging import argparse import colorama @@ -74,26 +75,56 @@ sys.exit(exit_code) -class PreParsedChefArgs(object): - def __init__(self, root=None, cache=True, debug=False, quiet=False, - log_file=None, log_debug=False, config_variant=None): - self.root = root - self.cache = cache - self.debug = debug - self.quiet = quiet - self.log_file = log_file - self.log_debug = log_debug - self.config_variant = config_variant - self.config_values = [] - self.debug_only = [] - - -def _parse_config_value(arg): - try: - name, value = arg.split('=') - except Exception: - raise Exception("Invalid configuration name and value: %s" % arg) - return (name, value) +def _setup_main_parser_arguments(parser): + parser.add_argument( + '--version', + action='version', + version=('%(prog)s ' + APP_VERSION)) + parser.add_argument( + '--root', + help="The root directory of the website.") + parser.add_argument( + '--config', + dest='config_variant', + help="The configuration variant to use for this command.") + parser.add_argument( + '--config-set', + nargs=2, + metavar=('NAME', 'VALUE'), + action='append', + dest='config_values', + help="Sets a specific site configuration setting.") + parser.add_argument( + '--debug', + help="Show debug information.", action='store_true') + parser.add_argument( + '--debug-only', + nargs='*', + help="Only show debug information for the given categories.") + parser.add_argument( + '--no-cache', + help="When applicable, disable caching.", + action='store_true') + parser.add_argument( + '--quiet', + help="Print only important information.", + action='store_true') + parser.add_argument( + '--log', + dest='log_file', + help="Send log messages to the specified file.") + parser.add_argument( + '--log-debug', + help="Log debug messages to the log file.", + action='store_true') + parser.add_argument( + '--no-color', + help="Don't use colorized output.", + action='store_true') + parser.add_argument( + '--pid-file', + dest='pid_file', + help="Write a PID file for the current process.") def _pre_parse_chef_args(argv): @@ -101,57 +132,28 @@ # parser, because it can affect which plugins will be loaded. Also, log- # related arguments must be parsed first because we want to log everything # from the beginning. - res = PreParsedChefArgs() - i = 0 - while i < len(argv): - arg = argv[i] - if arg.startswith('--root='): - res.root = os.path.expanduser(arg[len('--root='):]) - elif arg == '--root': - res.root = os.path.expanduser(argv[i + 1]) - i += 1 - elif arg.startswith('--config='): - res.config_variant = arg[len('--config='):] - elif arg == '--config': - res.config_variant = argv[i + 1] - i += 1 - elif arg.startswith('--config-set='): - res.config_values.append( - _parse_config_value(arg[len('--config-set='):])) - elif arg == '--config-set': - res.config_values.append(_parse_config_value(argv[i + 1])) - i += 1 - elif arg == '--log': - res.log_file = argv[i + 1] - i += 1 - elif arg == '--log-debug': - res.log_debug = True - elif arg == '--debug-only': - res.debug_only.append(argv[i + 1]) - i += 1 - elif arg == '--no-cache': - res.cache = False - elif arg == '--debug': - res.debug = True - elif arg == '--quiet': - res.quiet = True - else: - break - - i = i + 1 + parser = argparse.ArgumentParser() + _setup_main_parser_arguments(parser) + parser.add_argument('args', nargs=argparse.REMAINDER) + res, _ = parser.parse_known_args(argv) # Setup the logger. if res.debug and res.quiet: raise Exception("You can't specify both --debug and --quiet.") - colorama.init() + strip_colors = None + if res.no_color: + strip_colors = True + + colorama.init(strip=strip_colors) root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) if res.debug or res.log_debug: root_logger.setLevel(logging.DEBUG) - for n in res.debug_only: - logging.getLogger(n).setLevel(logging.DEBUG) + if res.debug_only: + for n in res.debug_only: + logging.getLogger(n).setLevel(logging.DEBUG) log_handler = logging.StreamHandler(sys.stdout) if res.debug or res.debug_only: @@ -171,14 +173,28 @@ if res.log_debug: file_handler.setLevel(logging.DEBUG) + # PID file. + if res.pid_file: + try: + pid_file_dir = os.path.dirname(res.pid_file) + if pid_file_dir and not os.path.isdir(pid_file_dir): + os.makedirs(pid_file_dir) + + with open(res.pid_file, 'w') as fp: + fp.write(str(os.getpid())) + except OSError as ex: + raise Exception("Can't write PID file: %s" % res.pid_file) from ex + return res def _run_chef(pre_args, argv): # Setup the app. start_time = time.perf_counter() - root = pre_args.root - if root is None: + root = None + if pre_args.root: + root = os.path.expanduser(pre_args.root) + else: try: root = find_app_root() except SiteNotFoundError: @@ -187,62 +203,33 @@ if not root: app = NullPieCrust() else: - app = PieCrust(root, cache=pre_args.cache, debug=pre_args.debug) + app = PieCrust(root, cache=(not pre_args.no_cache), + debug=pre_args.debug) # Build a hash for a custom cache directory. cache_key = 'default' # Handle custom configurations. - if pre_args.config_variant is not None and not root: - raise SiteNotFoundError("Can't apply any variant.") + if (pre_args.config_variant or pre_args.config_values) and not root: + raise SiteNotFoundError( + "Can't apply any configuration variant or value overrides, " + "there is no website here.") apply_variant_and_values(app, pre_args.config_variant, pre_args.config_values) # Adjust the cache key. if pre_args.config_variant is not None: cache_key += ',variant=%s' % pre_args.config_variant - for name, value in pre_args.config_values: - cache_key += ',%s=%s' % (name, value) + if pre_args.config_values: + for name, value in pre_args.config_values: + cache_key += ',%s=%s' % (name, value) # Setup the arg parser. parser = argparse.ArgumentParser( prog='chef', description="The PieCrust chef manages your website.", formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument( - '--version', - action='version', - version=('%(prog)s ' + APP_VERSION)) - parser.add_argument( - '--root', - help="The root directory of the website.") - parser.add_argument( - '--config', - help="The configuration variant to use for this command.") - parser.add_argument( - '--config-set', - help="Sets a specific site configuration setting.") - parser.add_argument( - '--debug', - help="Show debug information.", action='store_true') - parser.add_argument( - '--debug-only', - help="Only show debug information for the given categories.") - parser.add_argument( - '--no-cache', - help="When applicable, disable caching.", - action='store_true') - parser.add_argument( - '--quiet', - help="Print only important information.", - action='store_true') - parser.add_argument( - '--log', - help="Send log messages to the specified file.") - parser.add_argument( - '--log-debug', - help="Log debug messages to the log file.", - action='store_true') + _setup_main_parser_arguments(parser) commands = sorted(app.plugin_loader.getCommands(), key=lambda c: c.name)
--- a/piecrust/osutil.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/osutil.py Mon Feb 22 22:50:30 2016 -0800 @@ -9,7 +9,11 @@ glob = _system_glob.glob -if sys.platform == 'darwin': +def _wrap_fs_funcs(): + global walk + global listdir + global glob + def _walk(top, **kwargs): for dirpath, dirnames, filenames in os.walk(top, **kwargs): dirpath = _from_osx_fs(dirpath) @@ -33,9 +37,11 @@ def _to_osx_fs(s): return unicodedata.ucd_3_2_0.normalize('NFD', s) - global walk, listdir, glob - walk = _walk listdir = _listdir glob = _glob + +if sys.platform == 'darwin': + _wrap_fs_funcs() +
--- a/piecrust/page.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/page.py Mon Feb 22 22:50:30 2016 -0800 @@ -43,7 +43,7 @@ self.source_metadata = source_metadata self.rel_path = rel_path self._config = None - self._raw_content = None + self._segments = None self._flags = FLAG_NONE self._datetime = None @@ -74,9 +74,9 @@ return self._config @property - def raw_content(self): + def segments(self): self._load() - return self._raw_content + return self._segments @property def datetime(self): @@ -123,7 +123,7 @@ self._datetime = value def getSegment(self, name='content'): - return self.raw_content[name] + return self.segments[name] def _load(self): if self._config is not None: @@ -135,7 +135,7 @@ config.merge(self.source_metadata['config']) self._config = config - self._raw_content = content + self._segments = content if was_cache_valid: self._flags |= FLAG_RAW_CACHE_VALID
--- a/piecrust/plugins/base.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/plugins/base.py Mon Feb 22 22:50:30 2016 -0800 @@ -33,6 +33,9 @@ def getSources(self): return [] + def getPublishers(self): + return [] + def initialize(self, app): pass @@ -83,6 +86,9 @@ def getSources(self): return self._getPluginComponents('getSources') + def getPublishers(self): + return self._getPluginComponents('getPublishers') + def _ensureLoaded(self): if self._plugins is not None: return
--- a/piecrust/plugins/builtin.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/plugins/builtin.py Mon Feb 22 22:50:30 2016 -0800 @@ -1,10 +1,12 @@ from piecrust.commands.base import HelpCommand +from piecrust.commands.builtin.admin import AdministrationPanelCommand from piecrust.commands.builtin.baking import ( BakeCommand, ShowRecordCommand) from piecrust.commands.builtin.info import ( RootCommand, ShowConfigCommand, FindCommand, ShowSourcesCommand, ShowRoutesCommand, ShowPathsCommand) from piecrust.commands.builtin.plugins import PluginsCommand +from piecrust.commands.builtin.publishing import PublishCommand from piecrust.commands.builtin.scaffolding import ( PrepareCommand, DefaultPrepareTemplatesCommandExtension, @@ -32,6 +34,8 @@ from piecrust.processing.sass import SassProcessor from piecrust.processing.sitemap import SitemapProcessor from piecrust.processing.util import ConcatProcessor +from piecrust.publishing.shell import ShellCommandPublisher +from piecrust.publishing.rsync import RsyncPublisher from piecrust.sources.default import DefaultPageSource from piecrust.sources.posts import ( FlatPostsSource, ShallowPostsSource, HierarchyPostsSource) @@ -62,7 +66,9 @@ PluginsCommand(), BakeCommand(), ShowRecordCommand(), - ServeCommand()] + ServeCommand(), + AdministrationPanelCommand(), + PublishCommand()] def getCommandExtensions(self): return [ @@ -115,3 +121,8 @@ JekyllImporter(), WordpressXmlImporter()] + def getPublishers(self): + return [ + ShellCommandPublisher, + RsyncPublisher] +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/publishing/base.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,103 @@ +import os.path +import shlex +import logging +import threading +import subprocess + + +logger = logging.getLogger(__name__) + + +class PublishingContext(object): + def __init__(self): + self.bake_out_dir = None + self.preview = False + + +class Publisher(object): + def __init__(self, app, target): + self.app = app + self.target = target + self.parsed_url = None + self.log_file_path = None + + @property + def has_url_config(self): + return self.parsed_url is not None + + @property + def url_config(self): + if self.parsed_url is not None: + return self.getConfig() + raise Exception("This publisher has a full configuration.") + + def getConfig(self): + return self.app.config.get('publish/%s' % self.target) + + def getConfigValue(self, name): + if self.has_url_config: + raise Exception("This publisher only has a URL configuration.") + return self.app.config.get('publish/%s/%s' % (self.target, name)) + + def run(self, ctx): + raise NotImplementedError() + + +class ShellCommandPublisherBase(Publisher): + def __init__(self, app, target): + super(ShellCommandPublisherBase, self).__init__(app, target) + self.expand_user_args = True + + def run(self, ctx): + args = self._getCommandArgs(ctx) + if self.expand_user_args: + args = [os.path.expanduser(i) for i in args] + + if ctx.preview: + preview_args = ' '.join([shlex.quote(i) for i in args]) + logger.info( + "Would run shell command: %s" % preview_args) + return True + + logger.debug( + "Running shell command: %s" % args) + + proc = subprocess.Popen( + args, cwd=self.app.root_dir, bufsize=0, + stdout=subprocess.PIPE) + + logger.debug("Running publishing monitor for PID %d" % proc.pid) + thread = _PublishThread(proc) + thread.start() + proc.wait() + thread.join() + + if proc.returncode != 0: + logger.error( + "Publish process returned code %d" % proc.returncode) + else: + logger.debug("Publish process returned successfully.") + + return proc.returncode == 0 + + def _getCommandArgs(self, ctx): + raise NotImplementedError() + + +class _PublishThread(threading.Thread): + def __init__(self, proc): + super(_PublishThread, self).__init__( + name='publish_monitor', daemon=True) + self.proc = proc + self.root_logger = logging.getLogger() + + def run(self): + for line in iter(self.proc.stdout.readline, b''): + line_str = line.decode('utf8') + logger.info(line_str.rstrip('\r\n')) + for h in self.root_logger.handlers: + h.flush() + + self.proc.communicate() + logger.debug("Publish monitor exiting.") +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/publishing/publisher.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,142 @@ +import os.path +import time +import logging +import urllib.parse +from piecrust.chefutil import format_timed +from piecrust.publishing.base import PublishingContext + + +logger = logging.getLogger(__name__) + + +class InvalidPublishTargetError(Exception): + pass + + +class PublishingError(Exception): + pass + + +class Publisher(object): + def __init__(self, app): + self.app = app + + def run(self, target, preview=False, log_file=None): + start_time = time.perf_counter() + + # Get the configuration for this target. + target_cfg = self.app.config.get('publish/%s' % target) + if not target_cfg: + raise InvalidPublishTargetError( + "No such publish target: %s" % target) + + target_type = None + bake_first = True + parsed_url = None + if isinstance(target_cfg, dict): + target_type = target_cfg.get('type') + if not target_type: + raise InvalidPublishTargetError( + "Publish target '%s' doesn't specify a type." % target) + bake_first = target_cfg.get('bake', True) + elif isinstance(target_cfg, str): + comps = urllib.parse.urlparse(target_cfg) + if not comps.scheme: + raise InvalidPublishTargetError( + "Publish target '%s' has an invalid target URL." % + target) + parsed_url = comps + target_type = find_publisher_name(self.app, comps.scheme) + if target_type is None: + raise InvalidPublishTargetError( + "No such publish target scheme: %s" % comps.scheme) + + # Setup logging stuff. + hdlr = None + root_logger = logging.getLogger() + if log_file and not preview: + logger.debug("Adding file handler for: %s" % log_file) + hdlr = logging.FileHandler(log_file, mode='w', encoding='utf8') + root_logger.addHandler(hdlr) + if not preview: + logger.info("Deploying to %s" % target) + else: + logger.info("Previewing deployment to %s" % target) + + # Bake first is necessary. + bake_out_dir = None + if bake_first: + bake_out_dir = os.path.join(self.app.cache_dir, 'pub', target) + if not preview: + bake_start_time = time.perf_counter() + logger.debug("Baking first to: %s" % bake_out_dir) + + from piecrust.baking.baker import Baker + baker = Baker(self.app, bake_out_dir) + rec1 = baker.bake() + + from piecrust.processing.pipeline import ProcessorPipeline + proc = ProcessorPipeline(self.app, bake_out_dir) + rec2 = proc.run() + + if not rec1.success or not rec2.success: + raise Exception( + "Error during baking, aborting publishing.") + logger.info(format_timed(bake_start_time, "Baked website.")) + else: + logger.info("Would bake to: %s" % bake_out_dir) + + # Create the appropriate publisher. + pub = None + for pub_cls in self.app.plugin_loader.getPublishers(): + if pub_cls.PUBLISHER_NAME == target_type: + pub = pub_cls(self.app, target) + break + if pub is None: + raise InvalidPublishTargetError( + "Publish target '%s' has invalid type: %s" % + (target, target_type)) + pub.parsed_url = parsed_url + + # Publish! + logger.debug( + "Running publish target '%s' with publisher: %s" % + (target, pub.PUBLISHER_NAME)) + pub_start_time = time.perf_counter() + + ctx = PublishingContext() + ctx.bake_out_dir = bake_out_dir + ctx.preview = preview + try: + success = pub.run(ctx) + except Exception as ex: + raise PublishingError( + "Error publishing to target: %s" % target) from ex + finally: + if hdlr: + root_logger.removeHandler(hdlr) + hdlr.close() + + if not success: + raise PublishingError( + "Unknown error publishing to target: %s" % target) + logger.info(format_timed( + pub_start_time, "Ran publisher %s" % pub.PUBLISHER_NAME)) + + logger.info(format_timed(start_time, 'Deployed to %s' % target)) + + +def find_publisher_class(app, scheme): + for pub_cls in app.plugin_loader.getPublishers(): + pub_sch = getattr(pub_cls, 'PUBLISHER_SCHEME', None) + if ('bake+%s' % pub_sch) == scheme: + return pub_cls + return None + + +def find_publisher_name(app, scheme): + pub_cls = find_publisher_class(app, scheme) + if pub_cls: + return pub_cls.PUBLISHER_NAME + return None +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/publishing/rsync.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,25 @@ +from piecrust.publishing.base import ShellCommandPublisherBase + + +class RsyncPublisher(ShellCommandPublisherBase): + PUBLISHER_NAME = 'rsync' + PUBLISHER_SCHEME = 'rsync' + + def _getCommandArgs(self, ctx): + if self.has_url_config: + orig = ctx.bake_out_dir + dest = self.parsed_url.netloc + self.parsed_url.path + else: + orig = self.getConfigValue('source', ctx.bake_our_dir) + dest = self.getConfigValue('destination') + + rsync_options = None + if not self.has_url_config: + rsync_options = self.getConfigValue('options') + if rsync_options is None: + rsync_options = ['-avc', '--delete'] + + args = ['rsync'] + rsync_options + args += [orig, dest] + return args +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/publishing/shell.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,15 @@ +import shlex +from piecrust.publishing.base import ShellCommandPublisherBase + + +class ShellCommandPublisher(ShellCommandPublisherBase): + PUBLISHER_NAME = 'shell' + + def _getCommandArgs(self, ctx): + target_cmd = self.getConfigValue('command') + if not target_cmd: + raise Exception("No command specified for publish target: %s" % + self.target) + args = shlex.split(target_cmd) + return args +
--- a/piecrust/rendering.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/rendering.py Mon Feb 22 22:50:30 2016 -0800 @@ -234,7 +234,8 @@ eis.pushPage(ctx.page, ctx) try: # Build the data for both segment and layout rendering. - page_data = _build_render_data(ctx) + with ctx.app.env.timerScope("BuildRenderData"): + page_data = _build_render_data(ctx) # Render content segments. ctx.setCurrentPass(PASS_FORMATTING) @@ -242,16 +243,17 @@ save_to_fs = True if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page: save_to_fs = False - if repo and not ctx.force_render: - render_result = repo.get( - ctx.uri, - lambda: _do_render_page_segments(ctx.page, page_data), - fs_cache_time=ctx.page.path_mtime, - save_to_fs=save_to_fs) - else: - render_result = _do_render_page_segments(ctx.page, page_data) - if repo: - repo.put(ctx.uri, render_result, save_to_fs) + with ctx.app.env.timerScope("PageRenderSegments"): + if repo and not ctx.force_render: + render_result = repo.get( + ctx.uri, + lambda: _do_render_page_segments(ctx.page, page_data), + fs_cache_time=ctx.page.path_mtime, + save_to_fs=save_to_fs) + else: + render_result = _do_render_page_segments(ctx.page, page_data) + if repo: + repo.put(ctx.uri, render_result, save_to_fs) # Render layout. page = ctx.page @@ -261,8 +263,11 @@ layout_name = page.source.config.get('default_layout', 'default') null_names = ['', 'none', 'nil'] if layout_name not in null_names: - build_layout_data(page, page_data, render_result['segments']) - layout_result = _do_render_layout(layout_name, page, page_data) + with ctx.app.env.timerScope("BuildRenderData"): + build_layout_data(page, page_data, render_result['segments']) + + with ctx.app.env.timerScope("PageRenderLayout"): + layout_result = _do_render_layout(layout_name, page, page_data) else: layout_result = { 'content': render_result['segments']['content'], @@ -279,7 +284,7 @@ layout_result['pass_info']) return rp except Exception as ex: - page_rel_path = os.path.relpath(page.path, ctx.app.root_dir) + page_rel_path = os.path.relpath(ctx.page.path, ctx.app.root_dir) raise Exception("Error rendering page: %s" % page_rel_path) from ex finally: ctx.setCurrentPass(PASS_NONE) @@ -344,7 +349,7 @@ engine = get_template_engine(app, engine_name) formatted_segments = {} - for seg_name, seg in page.raw_content.items(): + for seg_name, seg in page.segments.items(): seg_text = '' for seg_part in seg.parts: part_format = seg_part.fmt or format_name
--- a/piecrust/resources/server/piecrust-debug-info.css Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/resources/server/piecrust-debug-info.css Mon Feb 22 22:50:30 2016 -0800 @@ -38,9 +38,9 @@ .piecrust-debug-window { padding: 1em; text-align: left; - font-family: serif; + font-family: sans-serif; font-style: normal; - font-size: 1rem; + font-size: 14px; font-weight: normal; background: #a42; color: #fff;
--- a/piecrust/serving/server.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/serving/server.py Mon Feb 22 22:50:30 2016 -0800 @@ -72,11 +72,12 @@ class Server(object): def __init__(self, root_dir, debug=False, sub_cache_dir=None, enable_debug_info=True, - static_preview=True): + root_url='/', static_preview=True): self.root_dir = root_dir self.debug = debug self.sub_cache_dir = sub_cache_dir self.enable_debug_info = enable_debug_info + self.root_url = root_url self.static_preview = static_preview self._page_record = ServeRecord() self._out_dir = os.path.join(root_dir, CACHE_DIR, 'server') @@ -109,14 +110,15 @@ # Create the app for this request. app = get_app_for_server(self.root_dir, debug=self.debug, - sub_cache_dir=self.sub_cache_dir) + sub_cache_dir=self.sub_cache_dir, + root_url=self.root_url) if (app.config.get('site/enable_debug_info') and self.enable_debug_info and '!debug' in request.args): app.config.set('site/show_debug_info', True) # We'll serve page assets directly from where they are. - app.env.base_asset_url_format = '/_asset/%path%' + app.env.base_asset_url_format = self.root_url + '_asset/%path%' # Let's see if it can be a page asset. response = self._try_serve_page_asset(app, environ, request) @@ -140,7 +142,8 @@ raise InternalServerError(msg) from ex def _try_serve_asset(self, environ, request): - rel_req_path = request.path.lstrip('/').replace('/', os.sep) + offset = len(self.root_url) + rel_req_path = request.path[offset:].replace('/', os.sep) if request.path.startswith('/_cache/'): # Some stuff needs to be served directly from the cache directory, # like LESS CSS map files. @@ -156,10 +159,11 @@ return None def _try_serve_page_asset(self, app, environ, request): - if not request.path.startswith('/_asset/'): + if not request.path.startswith(self.root_url + '_asset/'): return None - full_path = os.path.join(app.root_dir, request.path[len('/_asset/'):]) + offset = len(self.root_url + '_asset/') + full_path = os.path.join(app.root_dir, request.path[offset:]) if not os.path.isfile(full_path): return None
--- a/piecrust/serving/util.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/serving/util.py Mon Feb 22 22:50:30 2016 -0800 @@ -16,11 +16,12 @@ logger = logging.getLogger(__name__) -def get_app_for_server(root_dir, debug=False, sub_cache_dir=None): +def get_app_for_server(root_dir, debug=False, sub_cache_dir=None, + root_url='/'): app = PieCrust(root_dir=root_dir, debug=debug) if sub_cache_dir: app._useSubCacheDir(sub_cache_dir) - app.config.set('site/root', '/') + app.config.set('site/root', root_url) app.config.set('server/is_serving', True) return app
--- a/piecrust/sources/base.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/sources/base.py Mon Feb 22 22:50:30 2016 -0800 @@ -109,6 +109,9 @@ def buildPageFactories(self): raise NotImplementedError() + def buildPageFactory(self, path): + raise NotImplementedError() + def resolveRef(self, ref_path): """ Returns the full path and source metadata given a source (relative) path, like a ref-spec.
--- a/piecrust/sources/default.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/sources/default.py Mon Feb 22 22:50:30 2016 -0800 @@ -4,7 +4,9 @@ from piecrust.sources.base import ( PageFactory, PageSource, InvalidFileSystemEndpointError, MODE_CREATING) -from piecrust.sources.interfaces import IListableSource, IPreparingSource +from piecrust.sources.interfaces import ( + IListableSource, IPreparingSource, IInteractiveSource, + InteractiveField) from piecrust.sources.mixins import SimplePaginationSourceMixin @@ -21,7 +23,8 @@ f not in ['Thumbs.db']) # Windows bullshit -class DefaultPageSource(PageSource, IListableSource, IPreparingSource, +class DefaultPageSource(PageSource, + IListableSource, IPreparingSource, IInteractiveSource, SimplePaginationSourceMixin): SOURCE_NAME = 'default' @@ -55,6 +58,17 @@ self._populateMetadata(fac_path, metadata) yield PageFactory(self, fac_path, metadata) + def buildPageFactory(self, path): + if not path.startswith(self.fs_endpoint_path): + raise Exception("Page path '%s' isn't inside '%s'." % ( + path, self.fs_enpoint_path)) + rel_path = path[len(self.fs_endpoint_path):].lstrip('\\/') + slug = self._makeSlug(rel_path) + metadata = {'slug': slug} + fac_path = rel_path.replace('\\', '/') + self._populateMetadata(fac_path, metadata) + return PageFactory(self, fac_path, metadata) + def resolveRef(self, ref_path): path = os.path.normpath( os.path.join(self.fs_endpoint_path, ref_path.lstrip("\\/"))) @@ -134,6 +148,11 @@ def buildMetadata(self, args): return {'slug': args.uri} + def getInteractiveFields(self): + return [ + InteractiveField('slug', InteractiveField.TYPE_STRING, + 'new-page')] + def _makeSlug(self, rel_path): slug, ext = os.path.splitext(rel_path) slug = slug.replace('\\', '/')
--- a/piecrust/sources/interfaces.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/sources/interfaces.py Mon Feb 22 22:50:30 2016 -0800 @@ -23,7 +23,7 @@ raise NotImplementedError() -class IListableSource: +class IListableSource(object): """ Defines the interface for a source that can be iterated on in a hierarchical manner, for use with the `family` data endpoint. """ @@ -37,7 +37,7 @@ raise NotImplementedError() -class IPreparingSource: +class IPreparingSource(object): """ Defines the interface for a source whose pages can be created by the `chef prepare` command. """ @@ -48,3 +48,17 @@ raise NotImplementedError() +class InteractiveField(object): + TYPE_STRING = 0 + TYPE_INT = 1 + + def __init__(self, name, field_type, default_value): + self.name = name + self.field_type = field_type + self.default_value = default_value + + +class IInteractiveSource(object): + def getInteractiveFields(self): + raise NotImplementedError() +
--- a/piecrust/sources/posts.py Wed Dec 30 20:21:41 2015 +1300 +++ b/piecrust/sources/posts.py Mon Feb 22 22:50:30 2016 -0800 @@ -7,14 +7,17 @@ from piecrust.sources.base import ( PageSource, InvalidFileSystemEndpointError, PageFactory, MODE_CREATING, MODE_PARSING) -from piecrust.sources.interfaces import IPreparingSource +from piecrust.sources.interfaces import ( + IPreparingSource, IInteractiveSource, InteractiveField) from piecrust.sources.mixins import SimplePaginationSourceMixin +from piecrust.uriutil import multi_replace logger = logging.getLogger(__name__) -class PostsSource(PageSource, IPreparingSource, SimplePaginationSourceMixin): +class PostsSource(PageSource, IPreparingSource, IInteractiveSource, + SimplePaginationSourceMixin): PATH_FORMAT = None def __init__(self, app, name, config): @@ -33,6 +36,36 @@ metadata = self._parseMetadataFromPath(ref_path) return path, metadata + def buildPageFactory(self, path): + if not path.startswith(self.fs_endpoint_path): + raise Exception("Page path '%s' isn't inside '%s'." % ( + path, self.fs_endpoint_path)) + rel_path = path[len(self.fs_endpoint_path):].lstrip('\\/') + pat = self.PATH_FORMAT % { + 'year': 'YEAR', + 'month': 'MONTH', + 'day': 'DAY', + 'slug': 'SLUG', + 'ext': 'EXT'} + pat = re.escape(pat) + pat = multi_replace(pat, { + 'YEAR': '(\d{4})', + 'MONTH': '(\d{2})', + 'DAY': '(\d{2})', + 'SLUG': '(.*)', + 'EXT': '(.*)'}) + m = re.match(pat, rel_path) + if m is None: + raise Exception("'%s' isn't a proper %s page path." % ( + rel_path, self.SOURCE_NAME)) + return self._makeFactory( + rel_path, + m.group(4), + int(m.group(1)), + int(m.group(2)), + int(m.group(3))) + + def findPageFactory(self, metadata, mode): year = metadata.get('year') month = metadata.get('month') @@ -98,7 +131,8 @@ return PageFactory(self, rel_path, fac_metadata) def setupPrepareParser(self, parser, app): - parser.add_argument('-d', '--date', help="The date of the post, " + parser.add_argument( + '-d', '--date', help="The date of the post, " "in `year/month/day` format (defaults to today).") parser.add_argument('slug', help="The URL slug for the new post.") @@ -106,7 +140,7 @@ dt = datetime.date.today() if args.date: if args.date == 'today': - pass # Keep the default we had. + pass # Keep the default we had. elif args.date == 'tomorrow': dt += datetime.timedelta(days=1) elif args.date.startswith('+'): @@ -118,12 +152,21 @@ try: year, month, day = [int(s) for s in args.date.split('/')] except ValueError: - raise Exception("Dates must be of the form: YEAR/MONTH/DAY.") + raise Exception("Dates must be of the form: " + "YEAR/MONTH/DAY.") dt = datetime.date(year, month, day) year, month, day = dt.year, dt.month, dt.day return {'year': year, 'month': month, 'day': day, 'slug': args.slug} + def getInteractiveFields(self): + dt = datetime.date.today() + return [ + InteractiveField('year', InteractiveField.TYPE_INT, dt.year), + InteractiveField('month', InteractiveField.TYPE_INT, dt.month), + InteractiveField('day', InteractiveField.TYPE_INT, dt.day), + InteractiveField('slug', InteractiveField.TYPE_STRING, 'new-post')] + def _checkFsEndpointPath(self): if not os.path.isdir(self.fs_endpoint_path): if self.ignore_missing_dir:
--- a/pytest.ini Wed Dec 30 20:21:41 2015 +1300 +++ b/pytest.ini Mon Feb 22 22:50:30 2016 -0800 @@ -1,3 +1,3 @@ [pytest] -norecursedirs = .* _darcs CVS .svn .git .hg *.egg-info dist build node_modules venv tmp* {args} +norecursedirs = .* _darcs CVS .svn .git .hg *.egg-info dist build bower_components node_modules venv tmp* _cache {args}
--- a/requirements.txt Wed Dec 30 20:21:41 2015 +1300 +++ b/requirements.txt Mon Feb 22 22:50:30 2016 -0800 @@ -1,5 +1,8 @@ +cffi==1.5.0 colorama==0.3.3 compressinja==0.0.2 +Flask==0.10.1 +Flask-Login==0.3.2 Jinja2==2.7.3 Markdown==2.6.2 MarkupSafe==0.23
--- a/setup.py Wed Dec 30 20:21:41 2015 +1300 +++ b/setup.py Mon Feb 22 22:50:30 2016 -0800 @@ -199,7 +199,7 @@ 'Programming Language :: Python :: 3' ], entry_points={'console_scripts': [ - 'chef = piecrust.main:main', + 'chef = piecrust.main:main' ]} )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tasks.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,15 @@ +from invoke import Collection, task, run +from garcon.benchsite import genbenchsite +from garcon.changelog import genchangelog +from garcon.documentation import gendocs +from garcon.messages import genmessages +from garcon.pypi import makerelease + + +ns = Collection() +ns.add_task(genbenchsite, name='benchsite') +ns.add_task(genchangelog, name='changelog') +ns.add_task(gendocs, name='docs') +ns.add_task(genmessages, name='messages') +ns.add_task(makerelease, name='release') +
--- a/tests/bakes/test_unicode_tags.yaml Wed Dec 30 20:21:41 2015 +1300 +++ b/tests/bakes/test_unicode_tags.yaml Mon Feb 22 22:50:30 2016 -0800 @@ -38,8 +38,28 @@ {% endfor %} pages/_index.md: '' outfiles: + tag/Это тэг.html: | + Pages in /tag/%D0%AD%D1%82%D0%BE%20%D1%82%D1%8D%D0%B3.html + Post 01 +--- +config: + site: + slugify_mode: lowercase,encode +in: + posts/2015-03-01_post01.md: | + --- + title: Post 01 + tags: [Это тэг] + --- + pages/_tag.md: | + Pages in {{pctagurl(tag)}} + {% for p in pagination.posts -%} + {{p.title}} + {% endfor %} + pages/_index.md: '' +outfiles: tag/это тэг.html: | - Pages in /tag/%D0%AD%D1%82%D0%BE%20%D1%82%D1%8D%D0%B3.html + Pages in /tag/%D1%8D%D1%82%D0%BE%20%D1%82%D1%8D%D0%B3.html Post 01 --- config:
--- a/tests/conftest.py Wed Dec 30 20:21:41 2015 +1300 +++ b/tests/conftest.py Mon Feb 22 22:50:30 2016 -0800 @@ -156,9 +156,9 @@ hdl = logging.StreamHandler(stream=memstream) logging.getLogger().addHandler(hdl) try: - from piecrust.main import PreParsedChefArgs, _run_chef - pre_args = PreParsedChefArgs( - root=fs.path('/kitchen')) + from piecrust.main import _pre_parse_chef_args, _run_chef + pre_args = _pre_parse_chef_args([ + '--root', fs.path('/kitchen')]) exit_code = _run_chef(pre_args, argv) finally: logging.getLogger().removeHandler(hdl) @@ -449,13 +449,32 @@ test_time_iso8601 = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(ctx.time)) - replacements = { - '%test_time_iso8601%': test_time_iso8601} - for token, repl in replacements.items(): - left = left.replace(token, repl) - right = right.replace(token, repl) + test_time_iso8601_pattern = '%test_time_iso8601%' + + left_time_indices = [] + i = -1 + while True: + i = left.find(test_time_iso8601_pattern, i + 1) + if i >= 0: + left_time_indices.append(i) + left = (left[:i] + test_time_iso8601 + + left[i + len(test_time_iso8601_pattern):]) + else: + break for i in range(min(len(left), len(right))): + if i in left_time_indices: + # This is where the time starts. Let's compare that the time + # values are within a few seconds of each other (usually 0 or 1). + right_time_str = right[i:i + len(test_time_iso8601)] + right_time = time.strptime(right_time_str, '%Y-%m-%dT%H:%M:%SZ') + left_time = time.gmtime(ctx.time) + difference = time.mktime(left_time) - time.mktime(right_time) + print("Got time difference: %d" % difference) + if abs(difference) <= 2: + print("(good enough, moving to end of timestamp)") + i += len(test_time_iso8601) + if left[i] != right[i]: start = max(0, i - 15) l_end = min(len(left), i + 15)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_appconfig.py Mon Feb 22 22:50:30 2016 -0800 @@ -0,0 +1,35 @@ +from piecrust.appconfig import PieCrustConfiguration + + +def test_config_default(): + values = {} + config = PieCrustConfiguration(values=values) + assert config.get('site/root') == '/' + assert len(config.get('site/sources')) == 3 # pages, posts, theme_pages + + +def test_config_default2(): + config = PieCrustConfiguration() + assert config.get('site/root') == '/' + assert len(config.get('site/sources')) == 3 # pages, posts, theme_pages + + +def test_config_site_override_title(): + values = {'site': {'title': "Whatever"}} + config = PieCrustConfiguration(values=values) + assert config.get('site/root') == '/' + assert config.get('site/title') == 'Whatever' + + +def test_config_site_add_source(): + values = {'site': { + 'sources': {'notes': {}}, + 'routes': [{'url': '/notes/%path:slug%', 'source': 'notes'}] + }} + config = PieCrustConfiguration(values=values) + # The order of routes is important. Sources, not so much. + assert list(map(lambda v: v['source'], config.get('site/routes'))) == [ + 'notes', 'posts', 'posts', 'posts', 'pages', 'theme_pages'] + assert list(config.get('site/sources').keys()) == [ + 'pages', 'posts', 'notes', 'theme_pages'] +
--- a/util/changelog/category_title.rst Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ - -1.%sub_num% %category% ----------------------- -
--- a/util/changelog/header.rst Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ - -######### -CHANGELOG -######### - -This is the changelog for PieCrust_. - -.. _PieCrust: http://bolt80.com/piecrust/ - -
--- a/util/changelog/version_title.rst Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ - -================================== -%num%. PieCrust %version% (%date%) -================================== -
--- a/util/generate_benchsite.py Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,215 +0,0 @@ -import io -import os -import os.path -import string -import random -import datetime -import argparse - - -def generateWord(min_len=1, max_len=10): - length = random.randint(min_len, max_len) - word = ''.join(random.choice(string.ascii_letters) for _ in range(length)) - return word - - -def generateSentence(words): - return ' '.join([generateWord() for i in range(words)]) - - -def generateDate(): - year = random.choice(range(1995, 2015)) - month = random.choice(range(1, 13)) - day = random.choice(range(1, 29)) - hours = random.choice(range(0, 24)) - minutes = random.choice(range(0, 60)) - seconds = random.choice(range(0, 60)) - return datetime.datetime( - year, month, day, hours, minutes, seconds) - - -def generateTitleAndSlug(): - title = generateSentence(8) - slug = title.replace(' ', '-').lower() - slug = ''.join(c for c in slug if c.isalnum() or c == '-') - return title, slug - - -class BenchmarkSiteGenerator(object): - def __init__(self, out_dir): - self.out_dir = out_dir - self.all_tags = [] - - def generatePost(self): - post_info = {} - title, slug = generateTitleAndSlug() - post_info.update({ - 'title': title, - 'slug': slug}) - post_info['description'] = generateSentence(20) - post_info['tags'] = random.choice(self.all_tags) - post_info['datetime'] = generateDate() - - buf = io.StringIO() - with buf: - para_count = random.randint(5, 10) - for i in range(para_count): - buf.write(generateSentence(random.randint(50, 100))) - buf.write('\n\n') - post_info['text'] = buf.getvalue() - - self.writePost(post_info) - - def initialize(self): - pass - - def writePost(self, post_info): - raise NotImplementedError() - - -class PieCrustBechmarkSiteGenerator(BenchmarkSiteGenerator): - def initialize(self): - posts_dir = os.path.join(self.out_dir, 'posts') - if not os.path.isdir(posts_dir): - os.makedirs(posts_dir) - - def writePost(self, post_info): - out_dir = os.path.join(self.out_dir, 'posts') - slug = post_info['slug'] - dtstr = post_info['datetime'].strftime('%Y-%m-%d') - with open('%s/%s_%s.md' % (out_dir, dtstr, slug), 'w', - encoding='utf8') as f: - f.write('---\n') - f.write('title: %s\n' % post_info['title']) - f.write('description: %s\n' % post_info['description']) - f.write('tags: [%s]\n' % post_info['tags']) - f.write('---\n') - - para_count = random.randint(5, 10) - for i in range(para_count): - f.write(generateSentence(random.randint(50, 100))) - f.write('\n\n') - - -class OctopressBenchmarkSiteGenerator(BenchmarkSiteGenerator): - def initialize(self): - posts_dir = os.path.join(self.out_dir, 'source', '_posts') - if not os.path.isdir(posts_dir): - os.makedirs(posts_dir) - - def writePost(self, post_info): - out_dir = os.path.join(self.out_dir, 'source', '_posts') - slug = post_info['slug'] - dtstr = post_info['datetime'].strftime('%Y-%m-%d') - with open('%s/%s-%s.markdown' % (out_dir, dtstr, slug), 'w', - encoding='utf8') as f: - f.write('---\n') - f.write('layout: post\n') - f.write('title: %s\n' % post_info['title']) - f.write('date: %s 12:00\n' % dtstr) - f.write('comments: false\n') - f.write('categories: [%s]\n' % post_info['tags']) - f.write('---\n') - - para_count = random.randint(5, 10) - for i in range(para_count): - f.write(generateSentence(random.randint(50, 100))) - f.write('\n\n') - - -class MiddlemanBenchmarkSiteGenerator(BenchmarkSiteGenerator): - def initialize(self): - posts_dir = os.path.join(self.out_dir, 'source') - if not os.path.isdir(posts_dir): - os.makedirs(posts_dir) - - def writePost(self, post_info): - out_dir = os.path.join(self.out_dir, 'source') - slug = post_info['slug'] - dtstr = post_info['datetime'].strftime('%Y-%m-%d') - with open('%s/%s-%s.html.markdown' % (out_dir, dtstr, slug), 'w', - encoding='utf8') as f: - f.write('---\n') - f.write('title: %s\n' % post_info['title']) - f.write('date: %s\n' % post_info['datetime'].strftime('%Y/%m/%d')) - f.write('tags: %s\n' % post_info['tags']) - f.write('---\n') - - para_count = random.randint(5, 10) - for i in range(para_count): - f.write(generateSentence(random.randint(50, 100))) - f.write('\n\n') - - -class HugoBenchmarkSiteGenerator(BenchmarkSiteGenerator): - def initialize(self): - posts_dir = os.path.join(self.out_dir, 'content', 'post') - if not os.path.isdir(posts_dir): - os.makedirs(posts_dir) - - def writePost(self, post_info): - out_dir = os.path.join(self.out_dir, 'content', 'post') - dtstr = post_info['datetime'].strftime('%Y-%m-%d_%H-%M-%S') - post_path = os.path.join(out_dir, '%s.md' % dtstr) - with open(post_path, 'w', encoding='utf8') as f: - f.write('+++\n') - f.write('title = "%s"\n' % post_info['title']) - f.write('description = "%s"\n' % post_info['description']) - f.write('categories = [\n "%s"\n]\n' % post_info['tags']) - f.write('date = "%s"\n' % post_info['datetime'].strftime( - "%Y-%m-%d %H:%M:%S-00:00")) - f.write('slug ="%s"\n' % post_info['slug']) - f.write('+++\n') - f.write(post_info['text']) - - -generators = { - 'piecrust': PieCrustBechmarkSiteGenerator, - 'octopress': OctopressBenchmarkSiteGenerator, - 'middleman': MiddlemanBenchmarkSiteGenerator, - 'hugo': HugoBenchmarkSiteGenerator - } - - -def main(): - parser = argparse.ArgumentParser( - prog='generate_benchsite', - description=("Generates a benchmark website with placeholder " - "content suitable for testing.")) - parser.add_argument( - 'engine', - help="The engine to generate the site for.", - choices=list(generators.keys())) - parser.add_argument( - 'out_dir', - help="The target directory for the website.") - parser.add_argument( - '-c', '--post-count', - help="The number of posts to create.", - type=int, - default=100) - parser.add_argument( - '--tag-count', - help="The number of tags to use.", - type=int, - default=30) - - result = parser.parse_args() - - print("Generating %d posts in %s..." % ( - result.post_count, result.out_dir)) - - if not os.path.exists(result.out_dir): - os.makedirs(result.out_dir) - - gen = generators[result.engine](result.out_dir) - gen.all_tags = [generateWord(3, 12) for _ in range(result.tag_count)] - gen.initialize() - - for i in range(result.post_count): - gen.generatePost() - - -if __name__ == '__main__': - main() -
--- a/util/generate_changelog.py Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,161 +0,0 @@ -import os -import os.path -import re -import sys -import subprocess - - -hg_log_template = ("{if(tags, '>>{tags};{date|shortdate}\n')}" - "{desc|firstline}\n\n") - -re_add_tag_changeset = re.compile('^Added tag [^\s]+ for changeset [\w\d]+$') -re_merge_pr_changeset = re.compile('^Merge pull request') -re_tag = re.compile('^\d+\.\d+\.\d+([ab]\d+)?(rc\d+)?$') -re_change = re.compile('^(\w+):') -re_clean_code_span = re.compile('([^\s])``([^\s]+)') - -category_commands = [ - 'chef', 'bake', 'find', 'help', 'import', 'init', 'paths', 'plugin', - 'plugins', 'prepare', 'purge', 'root', 'routes', 'serve', - 'showconfig', 'showrecord', 'sources', 'theme', 'themes'] -category_core = [ - 'internal', 'bug', 'templating', 'formatting', 'performance', - 'data', 'config', 'rendering', 'render', 'debug', 'reporting', - 'linker', 'pagination', 'routing', 'caching'] -category_project = ['build', 'cm', 'docs', 'tests', 'setup'] -categories = [ - ('commands', category_commands), - ('core', category_core), - ('project', category_project), - ('miscellaneous', None)] -category_names = list(map(lambda i: i[0], categories)) - - -def generate(): - out_file = 'CHANGELOG.rst' - if len(sys.argv) > 1: - out_file = sys.argv[1] - - print("Generating %s" % out_file) - - if not os.path.exists('.hg'): - raise Exception("You must run this script from the root of a " - "Mercurial clone of the PieCrust repository.") - hglog = subprocess.check_output([ - 'hg', 'log', - '--rev', 'reverse(::master)', - '--template', hg_log_template]) - hglog = hglog.decode('utf8') - - templates = _get_templates() - - with open(out_file, 'w') as fp: - fp.write(templates['header']) - - skip = False - in_desc = False - current_version = 0 - current_version_info = None - current_changes = None - for line in hglog.splitlines(): - if line == '': - skip = False - in_desc = False - continue - - if not in_desc and line.startswith('>>'): - tags, tag_date = line[2:].split(';') - if re_tag.match(tags): - if current_version > 0: - _write_version_changes( - templates, - current_version, current_version_info, - current_changes, fp) - - current_version += 1 - current_version_info = tags, tag_date - current_changes = {} - in_desc = True - else: - skip = True - continue - - if skip or current_version == 0: - continue - - if re_add_tag_changeset.match(line): - continue - if re_merge_pr_changeset.match(line): - continue - - m = re_change.match(line) - if m: - ch_type = m.group(1) - for cat_name, ch_types in categories: - if ch_types is None or ch_type in ch_types: - msgs = current_changes.setdefault(cat_name, []) - msgs.append(line) - break - else: - assert False, ("Change '%s' should have gone in the " - "misc. bucket." % line) - else: - msgs = current_changes.setdefault('miscellaneous', []) - msgs.append(line) - - if current_version > 0: - _write_version_changes( - templates, - current_version, current_version_info, - current_changes, fp) - - -def _write_version_changes(templates, version, version_info, changes, fp): - tokens = { - 'num': str(version), - 'version': version_info[0], - 'date': version_info[1]} - tpl = _multi_replace(templates['version_title'], tokens) - fp.write(tpl) - - for i, cat_name in enumerate(category_names): - msgs = changes.get(cat_name) - if not msgs: - continue - - tokens = { - 'sub_num': str(i), - 'category': cat_name.title()} - tpl = _multi_replace(templates['category_title'], tokens) - fp.write(tpl) - - for msg in msgs: - msg = msg.replace('`', '``').rstrip('\n') - msg = re_clean_code_span.sub(r'\1`` \2', msg) - fp.write('* ' + msg + '\n') - - -def _multi_replace(s, tokens): - for token in tokens: - s = s.replace('%%%s%%' % token, tokens[token]) - return s - - -def _get_templates(): - tpl_dir = os.path.join(os.path.dirname(__file__), 'changelog') - tpls = {} - for name in os.listdir(tpl_dir): - tpl = _get_template(os.path.join(tpl_dir, name)) - name_no_ext, _ = os.path.splitext(name) - tpls[name_no_ext] = tpl - return tpls - - -def _get_template(filename): - with open(filename, 'r', encoding='utf8') as fp: - return fp.read() - - -if __name__ == '__main__': - generate() -
--- a/util/generate_messages.cmd Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -@echo off -setlocal - -set CUR_DIR=%~dp0 -set CHEF=%CUR_DIR%..\bin\chef -set OUT_DIR=%CUR_DIR%..\piecrust\resources\messages -set ROOT_DIR=%CUR_DIR%messages - -%CHEF% --root=%ROOT_DIR% bake -o %OUT_DIR% -del %OUT_DIR%\index.html -
--- a/util/generate_messages.sh Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -#!/bin/sh - -CUR_DIR="$( cd "$( dirname "$0" )" && pwd )" -CHEF=${CUR_DIR}/../bin/chef -OUT_DIR=${CUR_DIR}/../piecrust/resources/messages -ROOT_DIR=${CUR_DIR}/messages - -$CHEF --root=$ROOT_DIR bake -o $OUT_DIR -rm ${OUT_DIR}/index.html
--- a/util/messages/config.yml Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -site: - title: PieCrust System Messages
--- a/util/messages/pages/_index.html Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ ---- -title: PieCrust System Messages ---- - -Here are the **PieCrust** system message pages: - -* [Requirements Not Met]({{ pcurl('requirements') }}) -* [Error]({{ pcurl('error') }}) -* [Not Found]({{ pcurl('error404') }}) -* [Critical Error]({{ pcurl('critical') }}) - -This very page you're reading, however, is only here for convenience.
--- a/util/messages/pages/critical.html Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ ---- -title: The Whole Kitchen Burned Down! -layout: error ---- -Something critically bad happened, and **PieCrust** needs to shut down. It's probably our fault. -
--- a/util/messages/pages/error.html Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ ---- -title: The Cake Just Burned! -layout: error ---- -
--- a/util/messages/pages/error404.html Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ ---- -title: Can't find the sugar! -layout: error ---- -It looks like the page you were trying to access does not exist around here. Try going somewhere else. -
--- a/util/messages/templates/default.html Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,70 +0,0 @@ -<!doctype html> -<html> -<head> - <title>{{ page.title }}</title> - <meta name="generator" content="PieCrust" /> - <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Lobster"> - <style> - body { - margin: 0; - padding: 1em; - background: #eee; - color: #000; - font-family: Georgia, serif; - } - h1 { - font-size: 4.5em; - font-family: Lobster, 'Trebuchet MS', Verdana, sans-serif; - text-align: center; - font-weight: bold; - margin-top: 0; - color: #333; - text-shadow: 0px 2px 5px rgba(0,0,0,0.3); - } - h2 { - font-size: 2.5em; - font-family: 'Lobster', 'Trebuchet MS', Verdana, sans-serif; - } - code { - background: #ddd; - padding: 0 0.2em; - } - #preamble { - font-size: 1.2em; - font-style: italic; - text-align: center; - margin-bottom: 0; - } - #container { - margin: 0 20%; - } - #content { - margin: 2em 1em; - } - .error-details { - color: #d11; - } - .note { - margin: 3em; - color: #888; - font-style: italic; - } - </style> -</head> -<body> - <div id="container"> - <div id="header"> - <p id="preamble">A Message From The Kitchen:</p> - <h1>{{ page.title }}</h1> - </div> - <hr /> - <div id="content"> - {% block content %} - {{ content|safe }} - {% endblock %} - </div> - <hr /> - {% block footer %}{% endblock %} - </div> -</body> -</html>
--- a/util/messages/templates/error.html Wed Dec 30 20:21:41 2015 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,27 +0,0 @@ -{% extends "default.html" %} - -{% block content %} -{{content|safe}} - -{# The following is `raw` because we want it to be in the - produced page, so it can then be templated on the fly - with the error messages #} -{% raw %} -{% if details %} -<div class="error-details"> - <p>Error details:</p> - <ul> - {% for desc in details %} - <li>{{ desc }}</li> - {% endfor %} - </ul> -</div> -{% endif %} -{% endraw %} -{% endblock %} - -{% block footer %} -{% pcformat 'textile' %} -p(note). You're seeing this because something wrong happend. To see detailed errors with callstacks, run chef with the @--debug@ parameter, append @?!debug@ to the URL, or initialize the @PieCrust@ object with @{'debug': true}@. On the other hand, to see you custom error pages, set the @site/display_errors@ setting to @false@. -{% endpcformat %} -{% endblock %}