summaryrefslogtreecommitdiff
path: root/docs/tutorials
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-05-27 23:01:11 +0000
committerChris McDonough <chrism@agendaless.com>2009-05-27 23:01:11 +0000
commit83dc23642c766caf10fed0c98984e60a6b08fc14 (patch)
tree455e681dd39651c070fd447b551b17ae535910b4 /docs/tutorials
parentcca321bd168286583535f54673ae9b81e028c6f1 (diff)
downloadpyramid-83dc23642c766caf10fed0c98984e60a6b08fc14.tar.gz
pyramid-83dc23642c766caf10fed0c98984e60a6b08fc14.tar.bz2
pyramid-83dc23642c766caf10fed0c98984e60a6b08fc14.zip
Add bfgwiki tutorial.
Diffstat (limited to 'docs/tutorials')
-rw-r--r--docs/tutorials/bfgwiki/authorization.rst337
-rw-r--r--docs/tutorials/bfgwiki/background.rst16
-rw-r--r--docs/tutorials/bfgwiki/basiclayout.rst104
-rw-r--r--docs/tutorials/bfgwiki/definingmodels.rst147
-rw-r--r--docs/tutorials/bfgwiki/definingviews.rst364
-rw-r--r--docs/tutorials/bfgwiki/distributing.rst96
-rw-r--r--docs/tutorials/bfgwiki/index.rst22
-rw-r--r--docs/tutorials/bfgwiki/installation.rst255
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/CHANGES.txt3
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/README.txt4
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/ez_setup.py276
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/setup.cfg2
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/setup.py49
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial.ini26
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/__init__.py2
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml8
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/models.py26
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/run.py22
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/edit.pt31
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/style.css109
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/templates/view.pt27
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/tests.py150
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/views.py102
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/who.ini40
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/wiki.passwd1
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/CHANGES.txt3
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/README.txt4
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/ez_setup.py276
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/setup.cfg2
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/setup.py47
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial.ini20
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/__init__.py2
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/configure.zcml17
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/models.py12
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/run.py17
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/tests.py73
-rw-r--r--docs/tutorials/bfgwiki/src/basiclayout/tutorial/views.py9
-rw-r--r--docs/tutorials/bfgwiki/src/models/CHANGES.txt3
-rw-r--r--docs/tutorials/bfgwiki/src/models/README.txt4
-rw-r--r--docs/tutorials/bfgwiki/src/models/ez_setup.py276
-rw-r--r--docs/tutorials/bfgwiki/src/models/setup.cfg2
-rw-r--r--docs/tutorials/bfgwiki/src/models/setup.py48
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial.ini20
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/__init__.py2
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/configure.zcml17
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/models.py22
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/run.py17
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/tests.py75
-rw-r--r--docs/tutorials/bfgwiki/src/models/tutorial/views.py9
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/CHANGES.txt3
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/README.txt4
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/ez_setup.py276
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/setup.cfg2
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/setup.py48
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial.ini21
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/__init__.py2
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/configure.zcml8
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/models.py22
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/run.py17
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/edit.pt30
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/style.css109
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/view.pt26
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/tests.py150
-rw-r--r--docs/tutorials/bfgwiki/src/viewdecorators/tutorial/views.py81
-rw-r--r--docs/tutorials/bfgwiki/src/views/CHANGES.txt3
-rw-r--r--docs/tutorials/bfgwiki/src/views/README.txt4
-rw-r--r--docs/tutorials/bfgwiki/src/views/ez_setup.py276
-rw-r--r--docs/tutorials/bfgwiki/src/views/setup.cfg2
-rw-r--r--docs/tutorials/bfgwiki/src/views/setup.py48
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial.ini21
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/__init__.py2
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/configure.zcml34
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/models.py22
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/run.py17
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/edit.pt30
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/static/style.css109
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/templates/view.pt26
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/tests.py150
-rw-r--r--docs/tutorials/bfgwiki/src/views/tutorial/views.py71
-rw-r--r--docs/tutorials/bfgwiki/viewdecorators.rst257
120 files changed, 8677 insertions, 0 deletions
diff --git a/docs/tutorials/bfgwiki/authorization.rst b/docs/tutorials/bfgwiki/authorization.rst
new file mode 100644
index 000000000..3b5c2e3de
--- /dev/null
+++ b/docs/tutorials/bfgwiki/authorization.rst
@@ -0,0 +1,337 @@
+====================
+Adding Authorization
+====================
+
+Our application currently allows anyone with access to the server to
+view, edit, and add pages to our wiki. For purposes of demonstration
+we'll change our application to allow people whom possess a specific
+username (`editor`) to add and edit wiki pages but we'll continue
+allowing anyone with access to the server to view pages.
+
+:mod:`repoze.bfg` provides a facility for *authorization*, but it
+relies on "upstream" software to provide *authentication* information.
+We're going to use a package named ``repoze.who`` to our setup, and
+we'll rely on it to give us authentication information.
+
+Adding a Dependency on ``repoze.who`` to Our ``setup.py`` File
+--------------------------------------------------------------
+
+We need to change our ``setup.py`` file, adding a dependency on the
+``repoze.who`` package. The ``repoze.who`` package provides a
+mechanism for providing *authentication* data via :term:`WSGI`
+middleware. We'll add the ``repoze.who`` package to our ``requires``
+list.
+
+The resulting setup.py file:
+
+.. literalinclude:: src/authorization/setup.py
+ :linenos:
+ :language: python
+
+Changing our ``tutorial.ini`` file to Include the ``repoze.who`` Middleware
+---------------------------------------------------------------------------
+
+In order to make use of the ``repoze.who`` middleware which provides
+authentication services, we need to wire it into our ``tutorial.ini``
+file. We'll add a ``[filter:who]`` section to our ``tutorial.ini``
+file and wire it into our pipeline. Our resulting ``tutorial.ini``
+file will look like so:
+
+.. literalinclude:: src/authorization/tutorial.ini
+ :linenos:
+ :language: ini
+
+Note that we added a ``who`` line to our pipeline. This refers to the
+``[filter:who]`` section above it. The ``[filter:who]`` section has a
+``use`` line that points at an egg entry point for configuring the
+repoze.who middleware via a config file. The ``config_file`` line
+points at an .ini config file named ``who.ini``. This file is assumed
+to live in the same directory as the ``tutorial.ini`` file. We'll
+need to create this file in order to get authentication working.
+
+Adding a ``who.ini`` File
+-------------------------
+
+We'll create a file in our package directory named ``who.ini``. It
+will have the following contents.
+
+.. literalinclude:: src/authorization/who.ini
+ :linenos:
+ :language: ini
+
+The ``[general]``, ``[identifiers]``, ``[authenticators]``, and
+``[challengers]`` section of this file are the meat of the
+configuration in this file.
+
+The ``[general]`` Section
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``[general]`` section configures the default "request classifier"
+and "challenge decider". For the purposes of this tutorial, it is not
+important that you understand these settings.
+
+The ``[identifiers]`` Section
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``[identifiers]`` section configures the identifier plugins that
+will be used for this application. In our case, our identifiers are
+both the ``form`` plugin (configured above the ``[identifiers]``
+section within ``[plugin:form]``) and the ``auth_tkt`` plugin
+(configured above the ``[identifiers]`` section within
+``[plugin:auth_tkt]``. The ``form`` identifier will only be used when
+the request is a "browser request" (for example, it *won't* be used
+when the request is an XML-RPC request).
+
+The ``[authenticators]`` Section
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``[authenticators]`` section configures the "authenticator"
+plugins that will be used in our setup. An authenticator plugin is
+one which checks a username and password provided by a user against a
+database of valid username/password combinations. We'll use an
+htpasswd file as this database. Since the ``htpasswd`` plugin
+requires a file, we'll need to add a ``wiki.passwd`` file to our
+``tutorial`` package with these contents:
+
+.. literalinclude:: src/authorization/wiki.passwd
+ :linenos:
+ :language: ini
+
+The ``[challengers]`` Section
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``[challengers]`` section configures a "challenger" which is a
+``repoze.who`` plugin which displays a login form. We'll use the
+standard ``repoze.who.plugins.form`` plugin for this, configured
+within the ``[plugin:form]`` section of the file.
+
+The ``[plugin:*]`` Sections
+---------------------------
+
+The ``[plugin:*]`` sections of the configuration file configure
+individual plugins used by the more general configuration sections
+(``[identifiers]``, ``[authenticators]``, ``[challengers]``). The
+``auth_tkt`` plugin is an identifier plugin which obtains credentials
+from a cookie, the ``form`` plugin is an identifier and challenger
+plugin which obtains credentials from a form post, the ``htpasswd``
+plugin is an authenticator plugin which checks credentials against
+valid usernames and files specified in an htpasswd file.
+
+Configuring a ``repoze.bfg`` Authentication Policy
+--------------------------------------------------
+
+For any :mod:`repoze.bfg` application to perform authorization, we
+need to change our ``run.py`` module to add an :term:`authentication
+policy`. Adding an authentication policy causes the system to use
+authorization.
+
+Change your run.py to import the ``RepozeWho1AuthenticationPolicy``
+from ``repoze.who.authentication``, construct an instance of the
+policy, and pass it as the ``authentication_policy`` argument to the
+``make_app`` function. When you're done, your application's
+``run.py`` will look like this.
+
+.. literalinclude:: src/authorization/tutorial/run.py
+ :linenos:
+ :language: python
+
+Giving Our Root Model Object an ACL
+-----------------------------------
+
+We need to give our root model object an ACL. This ACL will be
+sufficient to provide enough information to the BFG security machinery
+to challenge a user who doesn't have appropriate credentials when he
+attempts to invoke the ``add_page`` or ``edit_page`` views.
+
+We need to perform some imports at module scope in our ``models.py``
+file:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.security import Allow
+ from repoze.bfg.security import Everyone
+
+Our root model is a ``Wiki`` object. We'll add the following line at
+class scope to our ``Wiki`` class:
+
+.. code-block:: python
+ :linenos:
+
+ __acl__ = [ (Allow, Everyone, 'view'), (Allow, 'editor', 'edit') ]
+
+It's only happenstance that we're assigning this ACL at class scope.
+An ACL can be attached to an object *instance* too; this is how "row
+level security" can be achieved in :mod:`repoze.bfg` applications. We
+actually only need *one* ACL for the entire system, however, because
+our security requirements are simple, so this feature is not
+demonstrated.
+
+Our resulting ``models.py`` file will now look like so:
+
+.. literalinclude:: src/authorization/tutorial/models.py
+ :linenos:
+ :language: python
+
+Adding ``permission`` Declarations to our ``bfg_view`` Decorators
+-----------------------------------------------------------------
+
+To protect each of our views with a particular permission, we need to
+pass a ``permission`` argument to each of our ``bfg_view`` decorators.
+To do so, within ``views.py``:
+
+- We add ``permission='view'`` to the ``bfg_view`` decorator attached
+ to the ``static_view`` view function. This makes the assertion that
+ only users who possess the effective ``view`` permission at the time
+ of the request may invoke this view. We've granted ``Everyone`` the
+ view permission at the root model via its ACL, so everyone will be
+ able to invoke the ``static_view`` view.
+
+- We add ``permission='view'`` to the ``bfg_view`` decorator attached
+ to the ``view_wiki`` view function. This makes the assertion that
+ only users who possess the effective ``view`` permission at the time
+ of the request may invoke this view. We've granted ``Everyone`` the
+ view permission at the root model via its ACL, so everyone will be
+ able to invoke the ``view_wiki`` view.
+
+- We add ``permission='view'`` to the ``bfg_view`` decorator attached
+ to the ``view_page`` view function. This makes the assertion that
+ only users who possess the effective ``view`` permission at the time
+ of the request may invoke this view. We've granted ``Everyone`` the
+ view permission at the root model via its ACL, so everyone will be
+ able to invoke the ``view_page`` view.
+
+- We add ``permission='edit'`` to the ``bfg_view`` decorator attached
+ to the ``add_page`` view function. This makes the assertion that
+ only users who possess the effective ``view`` permission at the time
+ of the request may invoke this view. We've granted ``editor`` the
+ view permission at the root model via its ACL, so only the user
+ named ``editor`` will able to invoke the ``add_page`` view.
+
+- We add ``permission='edit'`` to the ``bfg_view`` decorator attached
+ to the ``edit_page`` view function. This makes the assertion that
+ only users who possess the effective ``view`` permission at the time
+ of the request may invoke this view. We've granted ``editor`` the
+ view permission at the root model via its ACL, so only the user
+ named ``editor`` will able to invoke the ``edit_page`` view.
+
+Viewing the Application in a Browser
+------------------------------------
+
+Once we've set up the WSGI pipeline properly, we can finally examine
+our application in a browser. The views we'll try are as follows:
+
+- Visiting `http://localhost:6543/ <http://localhost:6543/>`_ in a
+ browser invokes the ``view_wiki`` view. This always redirects to
+ the ``view_page`` view of the FrontPage page object. It is
+ executable by any user.
+
+- Visiting `http://localhost:6543/FrontPage/
+ <http://localhost:6543/FrontPage/>`_ in a browser invokes the
+ ``view_page`` view of the front page page object. This is because
+ it's the *default view* (a view without a ``name``) for Page
+ objects. It is executable by any user.
+
+- Visiting `http://localhost:6543/FrontPage/edit_page
+ <http://localhost:6543/FrontPage/edit_page>`_ in a browser invokes
+ the edit view for the front page object. It is executable by only
+ the ``editor`` user. If a different user (or the anonymous user)
+ invokes it, a login form will be displayed. Supplying the
+ credentials with the username ``editor``, password ``editor`` will
+ show the edit page form being displayed.
+
+- Visiting `http://localhost:6543/add_page/SomePageName
+ <http://localhost:6543/add_page/SomePageName>`_ in a browser invokes
+ the add view for a page. It is executable by only the ``editor``
+ user. If a different user (or the anonymous user) invokes it, a
+ login form will be displayed. Supplying the credentials with the
+ username ``editor``, password ``editor`` will show the edit page
+ form being displayed.
+
+Add A Logout View
+-------------------
+
+We'll add a ``logout`` view to our application and provide a link to
+it. This view will clear the credentials of the logged in user and
+redirect back to the front page. The logout view will look someting
+like this:
+
+.. code-block:: python
+ :linenos:
+
+ @bfg_view(for_=Wiki, name='logout')
+ def logout(context, request):
+ identity = request.environ.get('repoze.who.identity')
+ headers = []
+ if identity is not None:
+ auth_tkt = request.environ['repoze.who.plugins']['auth_tkt']
+ headers = auth_tkt.forget(request.environ, identity)
+ return HTTPFound(location = model_url(context, request),
+ headers = headers)
+
+
+We'll also change our ``edit.pt`` template to display a "Logout" link
+if someone is logged in. This link will invoke the logout view.
+
+To do so we'll add this to both templates within the ``<div
+class="main_content">`` div:
+
+.. code-block:: xml
+ :linenos:
+
+ <span tal:condition="logged_in"><a href="${request.application_url}/logout">Logout</a></span>
+
+Then we need to change each opf our ``view_page``, ``edit_page`` and
+``add_page`` views to pass a "logged in" parameter into its template.
+We'll add something like this to each view body:
+
+.. code-block:: python
+ :linenos:
+
+ logged_in = 'repoze.who.identity' in request.environ
+
+We'll then change the return value of ``render_template_to_response``
+to pass the `resulting `logged_in`` value to the template, e.g.:
+
+.. code-block:: python
+ :linenos:
+
+ return render_template_to_response('templates/view.pt',
+ request = request,
+ page = context,
+ content = content,
+ logged_in = logged_in,
+ edit_url = edit_url)
+
+Seeing Our Changes To ``views.py`` and our Templates
+----------------------------------------------------
+
+Our ``views.py`` module will look something like this when we're done:
+
+.. literalinclude:: src/authorization/tutorial/views.py
+ :linenos:
+ :language: python
+
+Our ``edit.pt`` template will look something like this when we're done:
+
+.. literalinclude:: src/authorization/tutorial/templates/edit.pt
+ :linenos:
+ :language: xml
+
+Our ``view.pt`` template will look something like this when we're done:
+
+.. literalinclude:: src/authorization/tutorial/templates/view.pt
+ :linenos:
+ :language: xml
+
+Revisiting the Application
+---------------------------
+
+When we revisit the application in a browser, and log in (as a result
+of hitting an edit or add page and submitting the login form with the
+``editor`` credentials), we'll see a Logout link in the upper right
+hand corner. When we click it, we're logged out, and redirected back
+to the front page.
+
+
+
diff --git a/docs/tutorials/bfgwiki/background.rst b/docs/tutorials/bfgwiki/background.rst
new file mode 100644
index 000000000..9e458a9a1
--- /dev/null
+++ b/docs/tutorials/bfgwiki/background.rst
@@ -0,0 +1,16 @@
+==========
+Background
+==========
+
+To code along with the BFG wiki tutorial, the developer will need a
+UNIX machine with development tools (Mac OS X with XCode, any Linux or
+BSD variant, etc) *or* he will need a Windows system of any kind.
+
+The tutorial makes mention of a CDROM disk. This is due to the fact
+that the tutorial is taught "live" under some circumstances. When it
+mentions use of a CD, there are alternate instructions which follow
+for people who do not have the CD.
+
+This tutorial is known to work under :mod:`repoze.bfg` version 0.9a2.
+
+Have fun!
diff --git a/docs/tutorials/bfgwiki/basiclayout.rst b/docs/tutorials/bfgwiki/basiclayout.rst
new file mode 100644
index 000000000..1ec10094e
--- /dev/null
+++ b/docs/tutorials/bfgwiki/basiclayout.rst
@@ -0,0 +1,104 @@
+============
+Basic Layout
+============
+
+The starter files generated by the ``bfg_zodb`` template are basic,
+but they provide a good orientation for the high-level patterns common
+to most :term:`traversal` -based BFG (and BFG with ZODB) projects.
+
+``__init__.py``
+---------------
+
+A directory on disk can be turned into a Python :term:`package` by
+containing an ``__init__.py`` file. Even if empty, this marks a
+directory as a Python package.
+
+Configuration With ``configure.zcml``
+--------------------------------------
+
+BFG uses a markup language syntactically the same as Zope's
+implementation of :term:`ZCML`, but using a different default XML
+namespace. Our sample ZCML file looks like the following:
+
+ .. literalinclude:: src/basiclayout/tutorial/configure.zcml
+ :linenos:
+ :language: xml
+
+#. *Line 1*. The root ``<configure>`` element, in a *BFG* namespace.
+
+#. *Line 3*. Boilerplate, the comment explains.
+
+#. *Lines 6-9*. Register a ``<view>`` that is bound to a class.
+ ``.views.my_view`` is a *function* we write (generated by the
+ ``bfg_zodb`` template) that is given a ``context`` and a
+ ``request`` and returns a response.
+
+ Since this ``<view>`` doesn't have a ``name`` attribute, it is the
+ "default" view for that class.
+
+#. *Lines 11-15*. Register a view on the ``MyModels`` class that
+ answers URL segments of ``static``. This is a view that will serve
+ up static resources for us, in this case, at
+ ``http://localhost:6543/static/`` and below.
+
+Content Models with ``models.py``
+---------------------------------
+
+BFG often uses the word *model* when talking about content resources
+arranged in a hierarchical *model graph*. The ``models.py`` file is
+where the ``bfg_zodb`` Paster template put the classes that implement
+our models.
+
+Here is the source for ``models.py``:
+
+ .. literalinclude:: src/basiclayout/tutorial/models.py
+ :linenos:
+ :language: py
+
+#. *Lines 3-4*. The ``MyModel`` class we referred to in the ZCML is
+ implemented here. It is persistent (via PersistentMapping). The
+ ``__parent__`` and ``__name__`` are important parts of the
+ traversal protocol. By default, have these as ``None`` indicating
+ that this is the :term:`root` object.
+
+#. *Lines 6-12*. ``appmaker`` is used to return the *application
+ root* object. It is called on *every request* to the BFG
+ application (it is essentially a :term:`root factory`). It also
+ performs bootstrapping by *creating* an application root (inside
+ the ZODB root object) if one does not already exist.
+
+ We do so by first seeing if the database has the persistent
+ application root. If not, we make an instance, store it, and
+ commit the transaction. We then return the application root
+ object.
+
+App Startup with ``run.py``
+---------------------------
+
+How does a BFG application start up? When you run under ``paster``
+using the ``tutorial.ini`` generated config file, the application area
+points at an entry point. Our entry point happens to be in ``run.py``
+and its ``app`` function:
+
+ .. literalinclude:: src/basiclayout/tutorial/run.py
+ :linenos:
+ :language: py
+
+#. *Line 11*. After importing our application, get the ``appmaker``
+ function described above.
+
+#. *Line 12*. Get the ZODB configuration from the ``tutorial.ini``
+ file's ``[app:main]`` section. This will be a URI (something like
+ ``file:///path/to/Data.fs``).
+
+#. Line *16*. We create a :term:`root factory` using the
+ ``PersistentApplicationFinder`` helper class, passing it the
+ ZODB URI and our appmaker.
+
+#. Line *17*. We use the ``repoze.bfg.router.make_app`` to return a
+ :term:`WSGI` application. The ``make_app`` function takes the root
+ factory (``get_root``), the *package* representing our application,
+ and the keywords parsed by PasteDeploy.
+
+We'll later change ``run.py`` when we add :term:`authorization` to our
+wiki application.
diff --git a/docs/tutorials/bfgwiki/definingmodels.rst b/docs/tutorials/bfgwiki/definingmodels.rst
new file mode 100644
index 000000000..61eb5f112
--- /dev/null
+++ b/docs/tutorials/bfgwiki/definingmodels.rst
@@ -0,0 +1,147 @@
+===============
+Defining Models
+===============
+
+The first change we'll make to our bone-stock paster-generated
+application will be to define a number of :term:`model` constructors.
+For this application, which will be a Wiki, we will need two kinds of
+model constructors: a "Wiki" model constructor, and a "Page" model
+constructor. Both our Page and Wiki constructors will be class
+objects. A single instance of the "Wiki" class will serve as a
+container for "Page" objects, which will be instances of the "Page"
+class.
+
+Adding Model Classes
+--------------------
+
+The first thing we want to do is remove the ``MyModel`` class from the
+generated ``models.py`` file. The ``MyModel`` class is only a sample
+and we're not going to use it.
+
+.. note::
+
+ There is nothing automagically special about the filename
+ ``models.py``. A project may have many models throughout its
+ codebase in arbitrarily-named files. Files implementing models
+ often have ``model`` in their filenames (or they may live in a
+ Python subpackage of your application package named ``models``) ,
+ but this is only by convention.
+
+Then, we'll add a ``Wiki`` class. Because this is a ZODB application,
+this class should inherit from
+``persistent.mapping.PersistentMapping``. We want it to inherit from
+the ``PersistentMapping`` class because our Wiki class will be a
+mapping of wiki page names to ``Page`` objects. The
+``PersistentMapping`` class provides our class with mapping behavior,
+and makes sure that our Wiki page is stored as a "first-class"
+persistent object in our ZODB database.
+
+Our ``Wiki`` class should also have a ``__name__`` attribute set to
+``None`` at class scope, and should have a ``__parent__`` attribute
+set to None at class scope as well. If a model has a ``__parent__``
+attribute of ``None`` in a traversal-based :mod:`repoze.bfg`
+application, it means that it's the :term:`root` model. The
+``__name__`` of the root model is always ``None``.
+
+Then we'll add a ``Page`` class. This class should inherit from
+``persistent.Persistent``. We'll also give it an ``__init__`` method
+that accepts a single parameter named ``data``. This parameter will
+contain the :term:`ReStructuredText` body representing the wiki page
+content. Note that ``Page`` objects don't have an initial
+``__name__`` or ``__parent__`` attribute. All objects in a traversal
+graph must have a ``__name__`` and a ``__parent__`` attribute. We
+don't specify these here because both ``__name__`` and ``__parent__``
+will be set by by a :term:`view` function when a Page is added to our
+Wiki mapping.
+
+Add an Appmaker
+---------------
+
+We're using a mini-framework callable named
+``repoze.zodbconn.finder.PersistentApplicationFinder`` in our
+application (see "run.py"). A ``PersistentApplicationFinder`` accepts
+a ZODB URL as well as an "appmaker" callback. This callback typically
+lives in the ``models.py`` file.
+
+We want to change the appmaker function in our ``models.py`` file so
+that our application root is a Wiki instance, and we'll also slot a
+single page object (the front page) into the wiki.
+
+Looking at the Result of Our Edits to ``models.py``
+---------------------------------------------------
+
+The result of all of our edits to ``models.py`` will end up looking
+something like this:
+
+.. literalinclude:: src/models/tutorial/models.py
+ :linenos:
+ :language: python
+
+Testing the Models
+------------------
+
+To make sure the code we just wrote works, we write tests for the
+model classes and the appmaker. Changing ``tests.py``, we'll write a
+separate test class for each model class, and we'll write a test class
+for the ``appmaker``.
+
+To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided
+as a result of the ``bfg_zodb`` project generator but we'll disuse the
+``ViewIntegrationTests`` class. The ``ViewIntegrationTests`` class is
+too "heavy-hammer" for our tastes. We'll add three test classes: one
+for the ``Page`` model named ``PageModelTests``, one for the ``Wiki``
+model named ``WikiModelTests``, and one for the appmaker named
+``AppmakerTests``.
+
+When we're done changing ``tests.py``, it will look something like so:
+
+.. literalinclude:: src/models/tutorial/tests.py
+ :linenos:
+ :language: python
+
+Running the Tests
+-----------------
+
+We can run these tests by using ``setup.py test`` in the same way we
+did in :ref:`running_tests`. Assuming our shell's current working
+directory is the "tutorial" distribution directory:
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ ../bin/python setup.py test -q
+
+On Windows:
+
+.. code-block:: bash
+
+ c:\bigfntut\tutorial> ..\Scripts\python setup.py test -q
+
+The expected output is something like this:
+
+.. code-block:: bash
+
+ .....
+ ----------------------------------------------------------------------
+ Ran 5 tests in 0.008s
+
+ OK
+
+Declaring Dependencies in Our ``setup.py`` File
+-----------------------------------------------
+
+Our application depends on packages which are not dependencies of the
+original "tutorial" application as it was generated by the ``paster
+create`` command. We'll add these dependencies to our ``tutorial``
+package's ``setup.py`` file by assigning these dependencies to both
+the ``install_requires`` and the ``tests_require`` parameters to the
+``setup`` function. In particular, we require the ``docutils``
+package.
+
+Our resulting ``setup.py`` should look like so:
+
+.. literalinclude:: src/models/setup.py
+ :linenos:
+ :language: python
+
diff --git a/docs/tutorials/bfgwiki/definingviews.rst b/docs/tutorials/bfgwiki/definingviews.rst
new file mode 100644
index 000000000..bf47c37ad
--- /dev/null
+++ b/docs/tutorials/bfgwiki/definingviews.rst
@@ -0,0 +1,364 @@
+==============
+Defining Views
+==============
+
+Views in BFG are typically simple Python functions that accept two
+parameters: :term:`context`, and :term:`request`. A view is assumed
+to return a :term:`response` object.
+
+Adding View Functions
+=====================
+
+We're going to add four :term:`view` functions to our ``views.py``
+module. One view (named ``view_wiki``) will display the wiki itself
+(it will answer on the root URL), another named ``view_page`` will
+display an individual page, another named ``add_page`` will allow a
+page to be added, and a final view named ``edit_page`` will allow a
+page to be edited.
+
+.. note::
+
+ There is nothing automagically special about the filename
+ ``views.py``. A project may have many views throughout its codebase
+ in arbitrarily-named files. Files implementing views often have
+ ``view`` in their filenames (or may live in a Python subpackage of
+ your application package named ``views``), but this is only by
+ convention.
+
+
+The ``view_wiki`` view function
+-------------------------------
+
+The ``view_wiki`` function will respond as the default view of a
+``Wiki`` model object. It always redirects to the ``Page`` object
+named "FrontPage". It returns an instance of the
+``webob.exc.HTTPFound`` class (instances of which implement the WebOb
+:term:`response` interface), and the ``repoze.bfg.model_url`` API.
+``model_url`` constructs a URL to the ``FrontPage`` page
+(e.g. ``http://localhost:6543/FrontPage``), and uses it as the
+"location" of the HTTPFound response, forming an HTTP redirect.
+
+The ``view_page`` view function
+-------------------------------
+
+The ``view_page`` function will respond as the default view of a
+``Page`` object. The ``view_page`` function renders the
+:term:`ReStructuredText` body of a page (stored as the ``data``
+attribute of the context, which will be a Page object) as HTML. Then
+it substitutes an HTML anchor for each *WikiWord* reference in the
+rendered HTML using a compiled regular expression.
+
+The curried function named ``check`` is used as the first argument to
+``wikiwords.sub``, indicating that it should be called to provide a
+value for each WikiWord match found in the content. If the wiki (our
+page's ``__parent__``) already contains a page with the matched
+WikiWord name, the ``check`` function generates a view link to be used
+as the substitution value and returns it. If the wiki does not
+already contain a page with with the matched WikiWord name, the
+function generates an "add" link as the subsitution value and returns
+it.
+
+As a result, the ``content`` variable is now a fully formed bit of
+HTML containing various view and add links for WikiWords based on the
+content of our current page object.
+
+We then generate an edit URL (because it's easier to do here than in
+the template), and we call the
+``repoze.bfg.chameleon_zpt.render_template_to_response`` function with
+a number of arguments. The first argument is the *relative* path to a
+:term:`Chameleon` ZPT template. It is relative to the directory of
+the file in which we're creating the ``view_page`` function. The
+``render_template_to_response`` function also accepts ``request``,
+``page``, ``content``, and ``edit_url`` as keyword arguments. As a
+result, the template will be able to use these names to perform
+various rendering tasks.
+
+The result of ``render_template_to_response`` is returned to
+:mod:`repoze.bfg`. Unsurprisingly, it is a response object.
+
+The ``add_page`` view function
+------------------------------
+
+The ``add_page`` function will be invoked when a user clicks on a
+WikiWord which isn't yet represented as a page in the system. The
+``check`` function within the ``view_page`` view generates URLs to
+this view. It also acts as a handler for the form that is generated
+when we want to add a page object. The ``context`` of the
+``add_page`` view is always a Wiki object (*not* a Page object).
+
+The request :term:`subpath` in BFG is the sequence of names that are
+found *after* the view name in the URL segments given to BFG as the
+result of a request. If our add view is invoked via,
+e.g. ``http://localhost:6543/add_page/SomeName``, the :term:`subpath`
+will be ``['SomeName']``.
+
+The add view takes the zeroth element of the subpath (the wiki page
+name), and aliases it to the name attribute in order to know the name
+of the page we're trying to add.
+
+If the view rendering is *not* a result of a form submission (if the
+expression ``'form.submitted' in request.params`` is False), the view
+renders a template. To do so, it generates a "save url" which the
+template use as the form post URL during rendering. We're lazy here,
+so we're trying to use the same template (``templates/edit.pt``) for
+the add view as well as the page edit view, so we create a dummy Page
+object in order to satisfy the edit form's desire to have *some* page
+object exposed as ``page``, and we'll render the template to a
+response.
+
+If the view rendering *is* a result of a form submission (if the
+expression ``'form.submitted' in request.params`` is True), we scrape
+the page body from the form data, create a Page object using the name
+in the subpath and the page body, and save it into "our context" (the
+wiki) using the ``__setitem__`` method of the context. We then
+redirect back to the ``view_page`` view (the default view for a page)
+for the newly created page.
+
+The ``edit_page`` view function
+-------------------------------
+
+The ``edit_page`` function will be invoked when a user clicks the
+"Edit this Page" button on the view form. It renders an edit form but
+it also acts as the handler for the form it renders. The ``context``
+of the ``edit_page`` view will *always* be a Page object (never a Wiki
+object).
+
+If the view rendering is *not* a result of a form submission (if the
+expression ``'form.submitted' in request.params`` is False), the view
+simply renders the edit form, passing the request, the page object,
+and a save_url which will be used as the action of the generated form.
+
+If the view rendering *is* a result of a form submission (if the
+expression ``'form.submitted' in request.params`` is True), the view
+grabs the ``body`` element of the request parameter and sets it as the
+``data`` attribute of the page context. It then redirects to the
+default view of the context (the page), which will always be the
+``view_page`` view.
+
+Viewing the Result of Our Edits to ``views.py``
+===============================================
+
+The result of all of our edits to ``views.py`` will leave it looking
+like this:
+
+.. literalinclude:: src/views/tutorial/views.py
+ :linenos:
+ :language: python
+
+Adding Templates
+================
+
+The views we've added all reference a :term:`template`. Each template
+is a :term:`Chameleon` template. The default templating system in
+:mod:`repoze.bfg` is a variant of :term:`ZPT` provided by Chameleon.
+These templates will live in the ``templates`` directory of our
+tutorial package.
+
+The ``view.pt`` Template
+------------------------
+
+The ``view.pt`` template is used for viewing a single wiki page. It
+is used by the ``view_page`` view function. It should have a div that
+is "structure replaced" with the ``content`` value provided by the
+view. It should also have a link on the rendered page that points at
+the "edit" URL (the URL which invokes the ``edit_page`` view for the
+page being viewed).
+
+Once we're done with the ``view.pt`` template, it will look a lot like
+the below:
+
+.. literalinclude:: src/views/tutorial/templates/view.pt
+ :linenos:
+ :language: xml
+
+
+The ``edit.pt`` Template
+------------------------
+
+The ``edit.pt`` template is used for adding and editing a wiki page.
+It is used by the ``add_page`` and ``edit_page`` view functions. It
+should display a page containing a form that POSTs back to the
+"save_url" argument supplied by the view. The form should have a
+"body" textarea field (the page data), and a submit button that has
+the name "form.submitted". The textarea in the form should be filled
+with any existing page data when it is rendered.
+
+Once we're done with the ``edit.pt`` template, it will look a lot like
+the below:
+
+.. literalinclude:: src/views/tutorial/templates/edit.pt
+ :linenos:
+ :language: xml
+
+Static Resources
+----------------
+
+Our templates name a single static resource named ``style.css``. We
+need to create this and place it in a file named ``style.css`` within
+our package's ``templates/static`` directory:
+
+.. literalinclude:: src/views/tutorial/templates/static/style.css
+ :linenos:
+ :language: css
+
+This CSS file will be accessed via
+e.g. ``http://localhost:6543/static/style.css`` by virtue of the
+``static_view`` view we've defined in the ``views.py`` file. Any
+number and type of static resources can be placed in this directory
+(or subdirectories) and are just referred to by URL within templates.
+
+Testing the Views
+=================
+
+We'll modify our ``tests.py`` file, adding tests for each view
+function we added above. As a result, we'll *delete* the
+``ViewTests`` test in the file, and add four other test classes:
+``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and
+``EditPageTests``. These test the ``view_wiki``, ``view_page``,
+``add_page``, and ``edit_page`` views respectively.
+
+Once we're done with the ``tests.py`` module, it will look a lot like
+the below:
+
+.. literalinclude:: src/views/tutorial/tests.py
+ :linenos:
+ :language: python
+
+Running the Tests
+=================
+
+We can run these tests by using ``setup.py test`` in the same way we
+did in :ref:`running_tests`. Assuming our shell's current working
+directory is the "tutorial" distribution directory:
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ ../bin/python setup.py test -q
+
+On Windows:
+
+.. code-block:: bash
+
+
+ c:\bigfntut\tutorial> ..\Scripts\python setup.py test -q
+
+The expected result looks something like:
+
+.. code-block:: bash
+
+ .........
+ ----------------------------------------------------------------------
+ Ran 9 tests in 0.203s
+
+ OK
+
+Mapping Views to URLs in ``configure.zcml``
+===========================================
+
+The ``configure.zcml`` file contains ``view`` declarations which serve
+to map URLs (via :term:`traversal`) to view functions. You'll need to
+add five ``view`` declarations to ``configure.zcml``.
+
+#. Add a declaration which maps the "Wiki" class in our ``models.py``
+ file to the view named ``static_view`` in our ``views.py`` file with
+ the view name ``static``.
+
+#. Add a declaration which maps the "Wiki" class in our ``models.py``
+ file to the view named ``view_wiki`` in our ``views.py`` file with
+ no view name. This is the default view for a Wiki.
+
+#. Add a declaration which maps the "Page" class in our ``models.py``
+ file to the view named ``view_page`` in our ``views.py`` file with
+ no view name. This is the default view for a Page.
+
+#. Add a declaration which maps the "Wiki" class in our ``models.py``
+ file to the view named ``add_page`` in our ``views.py`` file with
+ the view name ``add_page``. This is the add view for a new Page.
+
+#. Add a declaration which maps the "Page" class in our ``models.py``
+ file to the view named ``edit_page`` in our ``views.py`` file with
+ the view name ``edit_page``. This is the edit view for a page.
+
+As a result of our edits, the ``configure.zcml`` file should look
+something like so:
+
+.. literalinclude:: src/views/tutorial/configure.zcml
+ :linenos:
+ :language: xml
+
+Examining ``tutorial.ini``
+==========================
+
+Let's take a look at our ``tutorial.ini`` file. The contents of the
+file are as follows:
+
+.. literalinclude:: src/models/tutorial.ini
+ :linenos:
+ :language: ini
+
+The WSGI Pipeline
+-----------------
+
+Within ``tutorial.ini``, note the existence of a ``[pipeline:main]``
+section which specifies our WSGI pipeline. This "pipeline" will be
+served up as our WSGI application. As far as the WSGI server is
+concerned the pipeline *is* our application. Simpler configurations
+don't use a pipeline: instead they expose a single WSGI application as
+"main". Our setup is more complicated, so we use a pipeline.
+
+"egg:repoze.zodbconn#closer" is at the "top" of the pipeline. This is
+a piece of middleware which closes the ZODB connection opened by the
+PersistentApplicationFinder at the end of the request.
+
+"egg:repoze.tm#tm" is the second piece of middleware in the pipeline.
+This commits a transaction near the end of the request unless there's
+an exception raised.
+
+Adding an Element to the Pipeline
+---------------------------------
+
+Let's add a piece of middleware to the WSGI pipeline.
+"egg:Paste#evalerror" middleware which displays debuggable errors in
+the browser while you're developing (not recommeded for deployment).
+Let's insert evalerror into the pipeline right below
+"egg:repoze.zodbconn#closer", making our resulting ``tutorial.ini``
+file look like so:
+
+.. literalinclude:: src/views/tutorial.ini
+ :linenos:
+ :language: ini
+
+Viewing the Application in a Browser
+====================================
+
+Once we've set up the WSGI pipeline properly, we can finally examine
+our application in a browser. The views we'll try are as follows:
+
+- Visiting `http://localhost:6543/ <http://localhost:6543/>`_ in a
+ browser invokes the ``view_wiki`` view. This always redirects to
+ the ``view_page`` view of the FrontPage page object.
+
+- Visiting `http://localhost:6543/FrontPage/
+ <http://localhost:6543/FrontPage/>`_ in a browser invokes the
+ ``view_page`` view of the front page page object. This is because
+ it's the *default view* (a view without a ``name``) for Page objects.
+
+- Visiting `http://localhost:6543/FrontPage/edit_page
+ <http://localhost:6543/FrontPage/edit_page>`_ in a browser invokes
+ the edit view for the front page object.
+
+- Visiting `http://localhost:6543/add_page/SomePageName
+ <http://localhost:6543/add_page/SomePageName>`_ in a browser invokes
+ the add view for a page.
+
+- To generate an error, do `http://localhost:6543/add_page
+ <http://localhost:6543/add_page>`_. IndexError for
+ ``request.subpath[0]``. You'll see an interactive traceback
+ facility provided by evalerror.
+
+
+
+
+
diff --git a/docs/tutorials/bfgwiki/distributing.rst b/docs/tutorials/bfgwiki/distributing.rst
new file mode 100644
index 000000000..2b99c9e3a
--- /dev/null
+++ b/docs/tutorials/bfgwiki/distributing.rst
@@ -0,0 +1,96 @@
+=============================
+Distributing Your Application
+=============================
+
+Once your application works properly, you can create a "tarball" from
+it by using the ``setup.py sdist`` command. The following commands
+assume your current working directory is the ``tutorial`` package
+we've created and that the parent directory of the ``tutorial``
+package is a virtualenv representing a BFG environment.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ ../bin/python setup.py sdist
+
+On Windows:
+
+.. code-block:: bash
+
+ c:\bigfntut> ..\Scripts\python setup.py sdist
+
+.. warning:: If your project files are not checked in to a version
+ control repository (such as Subversion), the dist tarball will
+ *not* contain all the files it needs to. In particular, it will
+ not contain non-Python-source files (such as templates and static
+ files). To ensure that these are included, check your files into a
+ version control repository before running ``setup.py sdist``.
+
+The output of such a command will be something like:
+
+.. code-block:: bash
+
+ running sdist
+ running egg_info
+ writing requirements to tutorial.egg-info/requires.txt
+ writing tutorial.egg-info/PKG-INFO
+ writing top-level names to tutorial.egg-info/top_level.txt
+ writing dependency_links to tutorial.egg-info/dependency_links.txt
+ writing entry points to tutorial.egg-info/entry_points.txt
+ writing manifest file 'tutorial.egg-info/SOURCES.txt'
+ warning: sdist: missing required meta-data: url
+ warning: sdist: missing meta-data: either (author and author_email) or (maintainer and maintainer_email) must be supplied
+ creating tutorial-0.1
+ creating tutorial-0.1/tutorial
+ creating tutorial-0.1/tutorial.egg-info
+ creating tutorial-0.1/tutorial/templates
+ creating tutorial-0.1/tutorial/templates/static
+ creating tutorial-0.1/tutorial/templates/static/images
+ making hard links in tutorial-0.1...
+ hard linking CHANGES.txt -> tutorial-0.1
+ hard linking README.txt -> tutorial-0.1
+ hard linking ez_setup.py -> tutorial-0.1
+ hard linking setup.cfg -> tutorial-0.1
+ hard linking setup.py -> tutorial-0.1
+ hard linking tutorial.ini -> tutorial-0.1
+ hard linking tutorial/__init__.py -> tutorial-0.1/tutorial
+ hard linking tutorial/configure.zcml -> tutorial-0.1/tutorial
+ hard linking tutorial/models.py -> tutorial-0.1/tutorial
+ hard linking tutorial/run.py -> tutorial-0.1/tutorial
+ hard linking tutorial/tests.py -> tutorial-0.1/tutorial
+ hard linking tutorial/views.py -> tutorial-0.1/tutorial
+ hard linking tutorial.egg-info/PKG-INFO -> tutorial-0.1/tutorial.egg-info
+ hard linking tutorial.egg-info/SOURCES.txt -> tutorial-0.1/tutorial.egg-info
+ hard linking tutorial.egg-info/dependency_links.txt -> tutorial-0.1/tutorial.egg-info
+ hard linking tutorial.egg-info/entry_points.txt -> tutorial-0.1/tutorial.egg-info
+ hard linking tutorial.egg-info/not-zip-safe -> tutorial-0.1/tutorial.egg-info
+ hard linking tutorial.egg-info/requires.txt -> tutorial-0.1/tutorial.egg-info
+ hard linking tutorial.egg-info/top_level.txt -> tutorial-0.1/tutorial.egg-info
+ hard linking tutorial/templates/edit.pt -> tutorial-0.1/tutorial/templates
+ hard linking tutorial/templates/mytemplate.pt -> tutorial-0.1/tutorial/templates
+ hard linking tutorial/templates/view.pt -> tutorial-0.1/tutorial/templates
+ hard linking tutorial/templates/static/default.css -> tutorial-0.1/tutorial/templates/static
+ hard linking tutorial/templates/static/style.css -> tutorial-0.1/tutorial/templates/static
+ hard linking tutorial/templates/static/templatelicense.txt -> tutorial-0.1/tutorial/templates/static
+ hard linking tutorial/templates/static/images/img01.gif -> tutorial-0.1/tutorial/templates/static/images
+ hard linking tutorial/templates/static/images/img02.gif -> tutorial-0.1/tutorial/templates/static/images
+ hard linking tutorial/templates/static/images/img03.gif -> tutorial-0.1/tutorial/templates/static/images
+ hard linking tutorial/templates/static/images/img04.gif -> tutorial-0.1/tutorial/templates/static/images
+ hard linking tutorial/templates/static/images/spacer.gif -> tutorial-0.1/tutorial/templates/static/images
+ copying setup.cfg -> tutorial-0.1
+ Writing tutorial-0.1/setup.cfg
+ creating dist
+ tar -cf dist/tutorial-0.1.tar tutorial-0.1
+ gzip -f9 dist/tutorial-0.1.tar
+ removing 'tutorial-0.1' (and everything under it)
+
+Note that this command creates a tarball in the "dist" subdirectory
+named ``tutorial-0.1.tar.gz``. You can send this file to your friends
+to show them your cool new application. They should be able to
+install it by pointing the ``easy_install`` command directly at it.
+Or you can upload it to `PyPI <http://pypi.python.org>`_ and share it
+with the rest of the world, where it can be downloaded via
+``easy_install`` remotely like any other package people download from
+PyPI.
+
diff --git a/docs/tutorials/bfgwiki/index.rst b/docs/tutorials/bfgwiki/index.rst
new file mode 100644
index 000000000..cabfa3d22
--- /dev/null
+++ b/docs/tutorials/bfgwiki/index.rst
@@ -0,0 +1,22 @@
+BFG Tutorial: BFG Wiki
+======================
+
+This tutorial introduces a :term:`traversal` -based BFG application to
+a developer with at least passing familiarity to Python. When we're
+done with the tutorial, the developer will have created a basic Wiki
+application with authentication.
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ background
+ installation
+ basiclayout
+ definingmodels
+ definingviews
+ viewdecorators
+ authorization
+ distributing
+
diff --git a/docs/tutorials/bfgwiki/installation.rst b/docs/tutorials/bfgwiki/installation.rst
new file mode 100644
index 000000000..ceeb83f06
--- /dev/null
+++ b/docs/tutorials/bfgwiki/installation.rst
@@ -0,0 +1,255 @@
+============
+Installation
+============
+
+For the most part, the installation process for this tutorial follows
+the `Installing repoze.bfg
+<http://docs.repoze.org/bfg/narr/install.html>`_ and `Creating a
+repoze.bfg Project <http://docs.repoze.org/bfg/narr/project.html>`_
+pages.
+
+Preparation (with CD)
+=====================
+
+Follow the instructions within the ``INSTALL.txt`` file found on the
+CD.
+
+Preparation (without CD)
+========================
+
+If you don't possess a CDROM with the tutorial files on it, take the
+following steps. The steps are slightly different depending on
+whether you're using UNIX or Windows.
+
+Preparation (without CD), UNIX
+------------------------------
+
+#. Obtain, install, or find `Python 2.5
+ <http://python.org/download/releases/2.5.4/>`_ for your system.
+
+#. Install latest `setuptools` into the Python you
+ obtained/installed/found in the step above: download `ez_setup.py
+ <http://peak.telecommunity.com/dist/ez_setup.py>`_ and run it using
+ the ``python`` interpreter of your Python 2.5 installation:
+
+ .. code-block:: bash
+
+ $ /path/to/my/Python-2.5/bin/python ez_setup.py
+
+#. Use that Python's `bin/easy_install` to install `virtualenv`:
+
+ .. code-block:: bash
+
+ $ /path/to/my/Python-2.5/bin/easy_install virtualenv
+
+#. Use that Python's virtualenv to make a workspace:
+
+ .. code-block:: bash
+
+ $ path/to/my/Python-25/bin/virtualenv --no-site-packages bigfntut
+
+#. Switch to the ``bigfntut`` directory:
+
+ .. code-block:: bash
+
+ $ cd bigfntut
+
+#. (Optional) Consider using ``source bin/activate`` to make your
+ shell environment wired to use the virtualenv.
+
+#. Use ``easy_install`` and point to the BFG "current" index to get
+ BFG and its direct dependencies installed:
+
+ .. code-block:: bash
+
+ $ bin/easy_install -i http://dist.repoze.org/bfg/current/simple repoze.bfg
+
+#. Use ``easy_install`` to install ``docutils``, ``repoze.tm``,
+ ``repoze.zodbconn``, ``repoze.who``, ``nose`` and ``coverage`` from
+ a *different* index (the "lemonade" index).
+
+ .. code-block:: bash
+
+ $ bin/easy_install -i http://dist.repoze.org/lemonade/dev/simple \
+ docutils repoze.tm repoze.zodbconn repoze.who nose coverage
+
+Preparation (without CD), Windows
+---------------------------------
+
+#. Install, or find `Python 2.5
+ <http://python.org/download/releases/2.5.4/>`_ for your system.
+
+#. Install latest `setuptools` into the Python you
+ obtained/installed/found in the step above: download `ez_setup.py
+ <http://peak.telecommunity.com/dist/ez_setup.py>`_ and run it using
+ the ``python`` interpreter of your Python 2.5 installation using a
+ command prompt:
+
+ .. code-block:: bash
+
+ c:\> c:\Python25\python ez_setup.py
+
+#. Use that Python's `bin/easy_install` to install `virtualenv`:
+
+ .. code-block:: bash
+
+ c:\> c:\Python25\Scripts\easy_install virtualenv
+
+#. Use that Python's virtualenv to make a workspace:
+
+ .. code-block:: bash
+
+ c:\> c:\Python25\Scripts\virtualenv --no-site-packages bigfntut
+
+#. Switch to the ``bigfntut`` directory:
+
+ .. code-block:: bash
+
+ c:\> cd bigfntut
+
+#. (Optional) Consider using ``bin\activate.bat`` to make your shell
+ environment wired to use the virtualenv.
+
+#. Use ``easy_install`` and point to the BFG "current index to get BFG
+ and its direct dependencies installed:
+
+ .. code-block:: bash
+
+ c:\bigfntut> Scripts/easy_install -i http://dist.repoze.org/bfg/current/simple repoze.bfg
+
+#. Use ``easy_install`` to install ``docutils``, ``repoze.tm``,
+ ``repoze.zodbconn``, ``repoze.who``, ``nose`` and ``coverage`` from
+ a *different* index (the "lemonade" index).
+
+ .. code-block:: bash
+
+ c:\bigfntut> Scripts\easy_install -i http://dist.repoze.org/lemonade/dev/simple docutils repoze.tm repoze.zodbconn repoze.who nose coverage
+
+.. _making_a_project:
+
+Making a Project
+================
+
+Whether you arrived at this point by installing your own environment
+using the steps above, or you used the instructions in the tutorial
+disc, your next steps are to create a project.
+
+BFG supplies a variety of templates to generate sample projects. We
+will use the :term:`ZODB` -oriented template.
+
+The below instructions assume your current working directory is the
+"virtualenv" named "bigfntut".
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ bin/paster create -t bfg_zodb tutorial
+
+On Windows:
+
+.. code-block:: bash
+
+ c:\bigfntut> Scripts\paster create -t bfg_zodb tutorial
+
+Installing the Project in "Development Mode"
+============================================
+
+In order to do development on the project easily, you must "register"
+the project as a development egg in your workspace using the
+``setup.py develop`` command. In order to do so, cd to the "tutorial"
+directory you created in :ref:`making_a_project`, and run the
+"setup.py develop" command using virtualenv Python interpreter.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ cd tutorial
+ $ ../bin/python setup.py develop
+
+On Windows:
+
+.. code-block:: bash
+
+ C:\bigfntut> cd tutorial
+ C:\bigfntut\tutorial> ..\Scripts\python setup.py develop
+
+.. _running_tests:
+
+Running the Tests
+=================
+
+After you've installed the project in development mode, you may run
+the tests for the project.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ ../bin/python setup.py test -q
+
+On Windows:
+
+.. code-block:: bash
+
+ c:\bigfntut\tutorial> ..\Scripts\python setup.py test -q
+
+Starting the Application
+========================
+
+Start the application.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ ../bin/paster serve tutorial.ini --reload
+
+On Windows:
+
+.. code-block:: bash
+
+ c:\bifgfntut\tutorial> ..\Scripts\paster serve tutorial.ini --reload
+
+Exposing Test Coverage Information
+==================================
+
+You can run the ``nosetests`` command to see test coverage
+information. This runs the tests in the same way that ``setup.py
+test`` does but provides additional "coverage" information, exposing
+which lines of your project are "covered" (or not covered) by the
+tests.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ ../bin/nosetests --cover-package=tutorial --cover-erase --with-coverage
+
+On Windows:
+
+.. code-block:: bash
+
+ c:\bigfntut\tutorial> ..\Scripts\nosetests --cover-package=tutorial --cover-erase --with-coverage
+
+Looks like the BFG template for ZODB projects is missing some test
+coverage, particularly in the file named ``models.py``.
+
+Visit the Application in a Browser
+==================================
+
+In a browser, visit `http://localhost:6543/ <http://localhost:6543>`_.
+You will see the generated application's default page.
+
+Decisions the ``bfg_zodb`` Template Has Made For You
+=====================================================
+
+Creating a project using the ``bfg_zodb`` template makes the
+assumption that you are willing to use :term:`ZODB` as persistent
+storage and :term:`traversal` to map URLs to code. BFG supports any
+persistent storage mechanism (e.g. a SQL database or filesystem files,
+etc), and supports an additional mechanism to map URLs to code
+(:term:`URL dispatch`). However, for the purposes of this tutorial,
+we'll be using traversal and ZODB.
+
diff --git a/docs/tutorials/bfgwiki/src/authorization/CHANGES.txt b/docs/tutorials/bfgwiki/src/authorization/CHANGES.txt
new file mode 100644
index 000000000..1544cf53b
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/CHANGES.txt
@@ -0,0 +1,3 @@
+0.1
+
+ Initial version
diff --git a/docs/tutorials/bfgwiki/src/authorization/README.txt b/docs/tutorials/bfgwiki/src/authorization/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/authorization/ez_setup.py b/docs/tutorials/bfgwiki/src/authorization/ez_setup.py
new file mode 100644
index 000000000..d24e845e5
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/ez_setup.py
@@ -0,0 +1,276 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ else:
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+ except pkg_resources.DistributionNotFound:
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/authorization/setup.cfg b/docs/tutorials/bfgwiki/src/authorization/setup.cfg
new file mode 100644
index 000000000..8ce7ae0fb
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/setup.cfg
@@ -0,0 +1,2 @@
+[easy_install]
+index_url = http://dist.repoze.org/bfg/current/simple
diff --git a/docs/tutorials/bfgwiki/src/authorization/setup.py b/docs/tutorials/bfgwiki/src/authorization/setup.py
new file mode 100644
index 000000000..6d300b473
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/setup.py
@@ -0,0 +1,49 @@
+import os
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.txt')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+requires = [
+ 'repoze.bfg',
+ 'docutils',
+ 'ZODB3',
+ 'repoze.zodbconn',
+ 'repoze.tm',
+ 'repoze.who',
+ ]
+
+setup(name='tutorial',
+ version='0.1',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+ "Topic :: Internet :: WWW/HTTP :: WSGI",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg zope',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=requires,
+ tests_require=requires,
+ test_suite="tutorial",
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial.ini b/docs/tutorials/bfgwiki/src/authorization/tutorial.ini
new file mode 100644
index 000000000..d30aa2672
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+debug = true
+
+[app:zodb]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+[filter:who]
+use = egg:repoze.who#config
+config_file = %(here)s/who.ini
+
+[pipeline:main]
+pipeline =
+ egg:repoze.zodbconn#closer
+ egg:Paste#evalerror
+ egg:repoze.tm#tm
+ who
+ zodb
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/__init__.py b/docs/tutorials/bfgwiki/src/authorization/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml b/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml
new file mode 100644
index 000000000..b1501597d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml
@@ -0,0 +1,8 @@
+<configure xmlns="http://namespaces.repoze.org/bfg">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="repoze.bfg.includes" />
+
+ <scan package="."/>
+
+</configure>
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/models.py b/docs/tutorials/bfgwiki/src/authorization/tutorial/models.py
new file mode 100644
index 000000000..976f5e3e9
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/models.py
@@ -0,0 +1,26 @@
+from persistent import Persistent
+from persistent.mapping import PersistentMapping
+
+from repoze.bfg.security import Allow
+from repoze.bfg.security import Everyone
+
+class Wiki(PersistentMapping):
+ __name__ = None
+ __parent__ = None
+ __acl__ = [ (Allow, Everyone, 'view'), (Allow, 'editor', 'edit') ]
+
+class Page(Persistent):
+ def __init__(self, data):
+ self.data = data
+
+def appmaker(zodb_root):
+ if not 'app_root' in zodb_root:
+ app_root = Wiki()
+ frontpage = Page('This is the front page')
+ app_root['FrontPage'] = frontpage
+ frontpage.__name__ = 'FrontPage'
+ frontpage.__parent__ = app_root
+ zodb_root['app_root'] = app_root
+ import transaction
+ transaction.commit()
+ return zodb_root['app_root']
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/run.py b/docs/tutorials/bfgwiki/src/authorization/tutorial/run.py
new file mode 100644
index 000000000..45920615f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/run.py
@@ -0,0 +1,22 @@
+from repoze.bfg.router import make_app
+from repoze.bfg.authentication import RepozeWho1AuthenticationPolicy
+from repoze.zodbconn.finder import PersistentApplicationFinder
+
+
+def app(global_config, **kw):
+ """ This function returns a repoze.bfg.router.Router object.
+
+ It is usually called by the PasteDeploy framework during ``paster serve``.
+ """
+ # paster app config callback
+ import tutorial
+ from tutorial.models import appmaker
+ zodb_uri = kw.get('zodb_uri')
+ if zodb_uri is None:
+ raise ValueError("No 'zodb_uri' in application configuration.")
+
+ authpolicy = RepozeWho1AuthenticationPolicy()
+
+ get_root = PersistentApplicationFinder(zodb_uri, appmaker)
+ return make_app(get_root, tutorial, authentication_policy=authpolicy,
+ options=kw)
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/edit.pt
new file mode 100644
index 000000000..d6b3ad466
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/edit.pt
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
+ <title>bfg tutorial wiki (based on TurboGears 20-Minute Wiki) Editing: ${page.__name__}</title>
+ <link rel="stylesheet" type="text/css"
+ href="${request.application_url}/static/style.css" />
+</head>
+
+<body>
+
+<div class="main_content">
+ <div style="float:right; width: 10em;"> Viewing
+ <span tal:replace="page.__name__">Page Name Goes Here</span> <br/>
+ You can return to the <a href="${request.application_url}">FrontPage</a>.
+ <span tal:condition="logged_in"><a href="${request.application_url}/logout">Logout</a></span>
+ </div>
+
+ <div>
+ <form action="${save_url}" method="post">
+ <textarea name="body" tal:content="page.data" rows="10" cols="60"/>
+ <input type="submit" name="form.submitted" value="Save"/>
+ </form>
+ </div>
+</div>
+</body>
+</html>
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..767252554
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/mytemplate.pt
@@ -0,0 +1,99 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<title>${project} Application</title>
+<meta name="keywords" content="python web application" />
+<meta name="description" content="repoze.bfg web application" />
+<link href="${request.application_url}/static/default.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+<!-- start header -->
+<div id="logo">
+ <h2><code>${project}</code>, a <code>repoze.bfg</code> application</h2>
+</div>
+<div id="header">
+ <div id="menu">
+ </div>
+</div>
+<!-- end header -->
+<div id="wrapper">
+ <!-- start page -->
+ <div id="page">
+ <!-- start content -->
+ <div id="content">
+ <div class="post">
+ <h1 class="title">Welcome to <code>${project}</code>, an
+ application generated by the <a
+ href="http://bfg.repoze.org">repoze.bfg</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>repoze.bfg</code> Documentation</h2>
+ <form method="get"
+ action="http://bfg.repoze.org/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>repoze.bfg</code> links</h2>
+ <ul>
+ <li><a
+ href="http://docs.repoze.org/bfg/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#repoze">IRC Channel</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <!-- end sidebar -->
+ <div style="clear: both;">&nbsp;</div>
+ </div>
+</div>
+<!-- end page -->
+<!-- start footer -->
+<div id="footer">
+ <p id="legal">( c ) 2008. All Rights Reserved. Template design
+ by <a href="http://www.freecsstemplates.org/">Free CSS
+ Templates</a>.</p>
+</div>
+<!-- end footer -->
+</body>
+</html>
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/default.css b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/default.css
@@ -0,0 +1,380 @@
+/*
+Design by Free CSS Templates
+http://www.freecsstemplates.org
+Released for free under a Creative Commons Attribution 2.5 License
+*/
+
+body {
+ margin: 0;
+ padding: 0;
+ background: url(images/img01.gif) repeat-x left top;
+ font-size: 13px;
+ font-family: "Trebuchet MS", Georgia, "Times New Roman", Times, serif;
+ text-align: justify;
+ color: #FFFFFF;
+}
+
+h1, h2, h3 {
+ margin: 0;
+ text-transform: lowercase;
+ font-weight: normal;
+ color: #FFFFFF;
+}
+
+h1 {
+ letter-spacing: -1px;
+ font-size: 32px;
+}
+
+h2 {
+ font-size: 23px;
+}
+
+p, ul, ol {
+ margin: 0 0 2em 0;
+ text-align: justify;
+ line-height: 26px;
+}
+
+a:link {
+ color: #8BD80E;
+}
+
+a:hover, a:active {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+a:visited {
+ color: #8BD80E;
+}
+
+img {
+ border: none;
+}
+
+img.left {
+ float: left;
+ margin-right: 15px;
+}
+
+img.right {
+ float: right;
+ margin-left: 15px;
+}
+
+/* Form */
+
+form {
+ margin: 0;
+ padding: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+legend {
+ display: none;
+}
+
+input, textarea, select {
+ font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
+ font-size: 13px;
+ color: #333333;
+}
+
+#wrapper {
+ margin: 0;
+ padding: 0;
+ background: #000000;
+}
+
+/* Header */
+
+#header {
+ width: 713px;
+ margin: 0 auto;
+ height: 42px;
+}
+
+/* Menu */
+
+#menu {
+ float: left;
+ width: 713px;
+ height: 50px;
+ background: url(images/img02.gif) no-repeat left top;
+}
+
+#menu ul {
+ margin: 0;
+ padding: 0px 0 0 10px;
+ list-style: none;
+ line-height: normal;
+}
+
+#menu li {
+ display: block;
+ float: left;
+}
+
+#menu a {
+ display: block;
+ float: left;
+ background: url(images/img04.gif) no-repeat right 55%;
+ margin-top: 5px;
+ margin-right: 3px;
+ padding: 8px 17px;
+ text-decoration: none;
+ font-size: 13px;
+ color: #000000;
+}
+
+#menu a:hover {
+ color: #000000;
+}
+
+#menu .current_page_item a {
+ color: #000000;
+}
+
+/** LOGO */
+
+#logo {
+ width: 713px;
+ height: 80px;
+ margin: 0 auto;
+}
+
+#logo h1, #logo h2 {
+ float: left;
+ margin: 0;
+ padding: 30px 0 0 0px;
+ line-height: normal;
+}
+
+#logo h1 {
+ font-family: Georgia, "Times New Roman", Times, serif;
+ font-size:40px;
+}
+
+#logo h1 a {
+ text-decoration: none;
+ color: #4C4C4C;
+}
+
+#logo h1 a:hover { text-decoration: underline; }
+
+#logo h2 {
+ float: left;
+ padding: 45px 0 0 18px;
+ font: 18px Georgia, "Times New Roman", Times, serif;
+ color: #8BD80E;
+}
+
+#logo p a {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+#logo p a:hover { text-decoration: underline; }
+
+
+
+/* Page */
+
+#page {
+ width: 663px;
+ margin: 0 auto;
+ background: #4C4C4C url(images/img03.gif) no-repeat left bottom;
+ padding: 0 25px;
+}
+
+/* Content */
+
+#content {
+ float: left;
+ width: 410px;
+
+}
+
+/* Post */
+
+.post {
+ padding: 15px 0px;
+ margin-bottom: 20px;
+}
+
+.post .title {
+ margin-bottom: 20px;
+ padding-bottom: 5px;
+}
+
+.post h1 {
+ padding: 0px 0 0 0px;
+ background: url(images/img08.jpg) no-repeat left top;
+ font-size: 24px;
+ color: #FFFFFF;
+}
+
+.post h2 {
+ padding: 0px 0 0 0px;
+ font-size: 22px;
+ color: #FFFFFF;
+}
+
+.post .entry {
+}
+
+.post .meta {
+ padding: 15px 15px 30px 0px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+}
+
+.post .meta p {
+ margin: 0;
+ padding-top: 15px;
+ line-height: normal;
+ color: #FFFFFF;
+}
+
+.post .meta .byline {
+ float: left;
+}
+
+.post .meta .links {
+ float: right;
+}
+
+.post .meta .more {
+ padding: 0 10px 0 18px;
+}
+
+.post .meta .comments {
+}
+
+.post .meta b {
+ display: none;
+}
+
+
+/* Sidebar */
+
+#sidebar {
+ width: 210px;
+ float: right;
+ margin: 0;
+ padding: 0;
+}
+
+#sidebar ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+#sidebar li {
+ margin-bottom: 40px;
+}
+
+#sidebar li ul {
+}
+
+#sidebar li li {
+ margin: 0;
+}
+
+#sidebar h2 {
+ width: 250px;
+ padding: 8px 0 0 0px;
+ margin-bottom: 10px;
+ background: url(images/img07.jpg) no-repeat left top;
+ font-size: 20px;
+ color: #FFFFFF;
+}
+
+/* Search */
+
+#search {
+
+}
+
+#search h2 {
+ margin-bottom: 20px;
+}
+
+#s {
+ width: 140px;
+ margin-right: 5px;
+ padding: 3px;
+ border: 1px solid #BED99C;
+}
+
+#x {
+ padding: 3px;
+ border: none;
+ background: #8BD80E;
+ text-transform: lowercase;
+ font-size: 11px;
+ color: #FFFFFF;
+}
+
+/* Boxes */
+
+.box1 {
+ padding: 20px;
+}
+
+.box2 {
+ color: #BABABA;
+}
+
+.box2 h2 {
+ margin-bottom: 15px;
+ font-size: 16px;
+ color: #FFFFFF;
+}
+
+.box2 ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.box2 a:link, .box2 a:hover, .box2 a:active, .box2 a:visited {
+ color: #EDEDED;
+}
+
+/* Footer */
+#footer-wrap {
+}
+
+#footer {
+ margin: 0 auto;
+ padding: 20px 0 10px 0;
+ background: #000000;
+}
+
+html>body #footer {
+ height: auto;
+}
+
+#footer p {
+ font-size: 11px;
+}
+
+#legal {
+ clear: both;
+ padding-top: 17px;
+ text-align: center;
+ color: #FFFFFF;
+}
+
+#legal a {
+ font-weight: normal;
+ color: #FFFFFF;
+}
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img01.gif b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img02.gif b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img03.gif b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img04.gif b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/spacer.gif b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/style.css b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/style.css
new file mode 100644
index 000000000..0a4b5767e
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/style.css
@@ -0,0 +1,109 @@
+html, body {
+ color: black;
+ background-color: #ddd;
+ font: x-small "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif;
+ margin: 0;
+ padding: 0;
+}
+
+td, th {padding:3px;border:none;}
+tr th {text-align:left;background-color:#f0f0f0;color:#333;}
+tr.odd td {background-color:#edf3fe;}
+tr.even td {background-color:#fff;}
+
+#header {
+ height: 80px;
+ width: 777px;
+ background: blue URL('../images/header_inner.png') no-repeat;
+ border-left: 1px solid #aaa;
+ border-right: 1px solid #aaa;
+ margin: 0 auto 0 auto;
+}
+
+a.link, a, a.active {
+ color: #369;
+}
+
+
+#main_content {
+ color: black;
+ font-size: 127%;
+ background-color: white;
+ width: 757px;
+ margin: 0 auto 0 auto;
+ border-left: 1px solid #aaa;
+ border-right: 1px solid #aaa;
+ padding: 10px;
+}
+
+#sidebar {
+ border: 1px solid #aaa;
+ background-color: #eee;
+ margin: 0.5em;
+ padding: 1em;
+ float: right;
+ width: 200px;
+ font-size: 88%;
+}
+
+#sidebar h2 {
+ margin-top: 0;
+}
+
+#sidebar ul {
+ margin-left: 1.5em;
+ padding-left: 0;
+}
+
+h1,h2,h3,h4,h5,h6,#getting_started_steps {
+ font-family: "Century Schoolbook L", Georgia, serif;
+ font-weight: bold;
+}
+
+h2 {
+ font-size: 150%;
+}
+
+#footer {
+ border: 1px solid #aaa;
+ border-top: 0px none;
+ color: #999;
+ background-color: white;
+ padding: 10px;
+ font-size: 80%;
+ text-align: center;
+ width: 757px;
+ margin: 0 auto 1em auto;
+}
+
+.code {
+ font-family: monospace;
+}
+
+span.code {
+ font-weight: bold;
+ background: #eee;
+}
+
+#status_block {
+ margin: 0 auto 0.5em auto;
+ padding: 15px 10px 15px 55px;
+ background: #cec URL('../images/ok.png') left center no-repeat;
+ border: 1px solid #9c9;
+ width: 450px;
+ font-size: 120%;
+ font-weight: bolder;
+}
+
+.notice {
+ margin: 0.5em auto 0.5em auto;
+ padding: 15px 10px 15px 55px;
+ width: 450px;
+ background: #eef URL('../images/info.png') left center no-repeat;
+ border: 1px solid #cce;
+}
+
+.fielderror {
+ color: red;
+ font-weight: bold;
+}
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/templatelicense.txt b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/static/templatelicense.txt
@@ -0,0 +1,243 @@
+Creative Commons </>
+
+Creative Commons Legal Code
+
+*Attribution 2.5*
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
+ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE
+INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ITS USE.
+
+/License/
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE
+RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS
+AND CONDITIONS.
+
+*1. Definitions*
+
+ 1. *"Collective Work"* means a work, such as a periodical issue,
+ anthology or encyclopedia, in which the Work in its entirety in
+ unmodified form, along with a number of other contributions,
+ constituting separate and independent works in themselves, are
+ assembled into a collective whole. A work that constitutes a
+ Collective Work will not be considered a Derivative Work (as
+ defined below) for the purposes of this License.
+ 2. *"Derivative Work"* means a work based upon the Work or upon the
+ Work and other pre-existing works, such as a translation, musical
+ arrangement, dramatization, fictionalization, motion picture
+ version, sound recording, art reproduction, abridgment,
+ condensation, or any other form in which the Work may be recast,
+ transformed, or adapted, except that a work that constitutes a
+ Collective Work will not be considered a Derivative Work for the
+ purpose of this License. For the avoidance of doubt, where the
+ Work is a musical composition or sound recording, the
+ synchronization of the Work in timed-relation with a moving image
+ ("synching") will be considered a Derivative Work for the purpose
+ of this License.
+ 3. *"Licensor"* means the individual or entity that offers the Work
+ under the terms of this License.
+ 4. *"Original Author"* means the individual or entity who created the
+ Work.
+ 5. *"Work"* means the copyrightable work of authorship offered under
+ the terms of this License.
+ 6. *"You"* means an individual or entity exercising rights under this
+ License who has not previously violated the terms of this License
+ with respect to the Work, or who has received express permission
+ from the Licensor to exercise rights under this License despite a
+ previous violation.
+
+*2. Fair Use Rights.* Nothing in this license is intended to reduce,
+limit, or restrict any rights arising from fair use, first sale or other
+limitations on the exclusive rights of the copyright owner under
+copyright law or other applicable laws.
+
+*3. License Grant.* Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ 1. to reproduce the Work, to incorporate the Work into one or more
+ Collective Works, and to reproduce the Work as incorporated in the
+ Collective Works;
+ 2. to create and reproduce Derivative Works;
+ 3. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission the Work including as incorporated in Collective Works;
+ 4. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission Derivative Works.
+ 5.
+
+ For the avoidance of doubt, where the work is a musical composition:
+
+ 1. *Performance Royalties Under Blanket Licenses*. Licensor
+ waives the exclusive right to collect, whether individually
+ or via a performance rights society (e.g. ASCAP, BMI,
+ SESAC), royalties for the public performance or public
+ digital performance (e.g. webcast) of the Work.
+ 2. *Mechanical Rights and Statutory Royalties*. Licensor waives
+ the exclusive right to collect, whether individually or via
+ a music rights agency or designated agent (e.g. Harry Fox
+ Agency), royalties for any phonorecord You create from the
+ Work ("cover version") and distribute, subject to the
+ compulsory license created by 17 USC Section 115 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+ 6. *Webcasting Rights and Statutory Royalties*. For the avoidance of
+ doubt, where the Work is a sound recording, Licensor waives the
+ exclusive right to collect, whether individually or via a
+ performance-rights society (e.g. SoundExchange), royalties for the
+ public digital performance (e.g. webcast) of the Work, subject to
+ the compulsory license created by 17 USC Section 114 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights
+in other media and formats. All rights not expressly granted by Licensor
+are hereby reserved.
+
+*4. Restrictions.*The license granted in Section 3 above is expressly
+made subject to and limited by the following restrictions:
+
+ 1. You may distribute, publicly display, publicly perform, or
+ publicly digitally perform the Work only under the terms of this
+ License, and You must include a copy of, or the Uniform Resource
+ Identifier for, this License with every copy or phonorecord of the
+ Work You distribute, publicly display, publicly perform, or
+ publicly digitally perform. You may not offer or impose any terms
+ on the Work that alter or restrict the terms of this License or
+ the recipients' exercise of the rights granted hereunder. You may
+ not sublicense the Work. You must keep intact all notices that
+ refer to this License and to the disclaimer of warranties. You may
+ not distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work with any technological measures that
+ control access or use of the Work in a manner inconsistent with
+ the terms of this License Agreement. The above applies to the Work
+ as incorporated in a Collective Work, but this does not require
+ the Collective Work apart from the Work itself to be made subject
+ to the terms of this License. If You create a Collective Work,
+ upon notice from any Licensor You must, to the extent practicable,
+ remove from the Collective Work any credit as required by clause
+ 4(b), as requested. If You create a Derivative Work, upon notice
+ from any Licensor You must, to the extent practicable, remove from
+ the Derivative Work any credit as required by clause 4(b), as
+ requested.
+ 2. If you distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work or any Derivative Works or Collective
+ Works, You must keep intact all copyright notices for the Work and
+ provide, reasonable to the medium or means You are utilizing: (i)
+ the name of the Original Author (or pseudonym, if applicable) if
+ supplied, and/or (ii) if the Original Author and/or Licensor
+ designate another party or parties (e.g. a sponsor institute,
+ publishing entity, journal) for attribution in Licensor's
+ copyright notice, terms of service or by other reasonable means,
+ the name of such party or parties; the title of the Work if
+ supplied; to the extent reasonably practicable, the Uniform
+ Resource Identifier, if any, that Licensor specifies to be
+ associated with the Work, unless such URI does not refer to the
+ copyright notice or licensing information for the Work; and in the
+ case of a Derivative Work, a credit identifying the use of the
+ Work in the Derivative Work (e.g., "French translation of the Work
+ by Original Author," or "Screenplay based on original Work by
+ Original Author"). Such credit may be implemented in any
+ reasonable manner; provided, however, that in the case of a
+ Derivative Work or Collective Work, at a minimum such credit will
+ appear where any other comparable authorship credit appears and in
+ a manner at least as prominent as such other comparable authorship
+ credit.
+
+*5. Representations, Warranties and Disclaimer*
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE
+EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+*6. Limitation on Liability.* EXCEPT TO THE EXTENT REQUIRED BY
+APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL
+THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY
+DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF
+LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+*7. Termination*
+
+ 1. This License and the rights granted hereunder will terminate
+ automatically upon any breach by You of the terms of this License.
+ Individuals or entities who have received Derivative Works or
+ Collective Works from You under this License, however, will not
+ have their licenses terminated provided such individuals or
+ entities remain in full compliance with those licenses. Sections
+ 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
+ 2. Subject to the above terms and conditions, the license granted
+ here is perpetual (for the duration of the applicable copyright in
+ the Work). Notwithstanding the above, Licensor reserves the right
+ to release the Work under different license terms or to stop
+ distributing the Work at any time; provided, however that any such
+ election will not serve to withdraw this License (or any other
+ license that has been, or is required to be, granted under the
+ terms of this License), and this License will continue in full
+ force and effect unless terminated as stated above.
+
+*8. Miscellaneous*
+
+ 1. Each time You distribute or publicly digitally perform the Work or
+ a Collective Work, the Licensor offers to the recipient a license
+ to the Work on the same terms and conditions as the license
+ granted to You under this License.
+ 2. Each time You distribute or publicly digitally perform a
+ Derivative Work, Licensor offers to the recipient a license to the
+ original Work on the same terms and conditions as the license
+ granted to You under this License.
+ 3. If any provision of this License is invalid or unenforceable under
+ applicable law, it shall not affect the validity or enforceability
+ of the remainder of the terms of this License, and without further
+ action by the parties to this agreement, such provision shall be
+ reformed to the minimum extent necessary to make such provision
+ valid and enforceable.
+ 4. No term or provision of this License shall be deemed waived and no
+ breach consented to unless such waiver or consent shall be in
+ writing and signed by the party to be charged with such waiver or
+ consent.
+ 5. This License constitutes the entire agreement between the parties
+ with respect to the Work licensed here. There are no
+ understandings, agreements or representations with respect to the
+ Work not specified here. Licensor shall not be bound by any
+ additional provisions that may appear in any communication from
+ You. This License may not be modified without the mutual written
+ agreement of the Licensor and You.
+
+Creative Commons is not a party to this License, and makes no warranty
+whatsoever in connection with the Work. Creative Commons will not be
+liable to You or any party on any legal theory for any damages
+whatsoever, including without limitation any general, special,
+incidental or consequential damages arising in connection to this
+license. Notwithstanding the foregoing two (2) sentences, if Creative
+Commons has expressly identified itself as the Licensor hereunder, it
+shall have all rights and obligations of Licensor.
+
+Except for the limited purpose of indicating to the public that the Work
+is licensed under the CCPL, neither party will use the trademark
+"Creative Commons" or any related trademark or logo of Creative Commons
+without the prior written consent of Creative Commons. Any permitted use
+will be in compliance with Creative Commons' then-current trademark
+usage guidelines, as may be published on its website or otherwise made
+available upon request from time to time.
+
+Creative Commons may be contacted at http://creativecommons.org/
+<http://creativecommons.org>.
+
+« Back to Commons Deed <./>
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/view.pt b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/view.pt
new file mode 100644
index 000000000..cae6940c2
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/templates/view.pt
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
+ <title>${page.__name__} - bfg tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+ <link rel="stylesheet" type="text/css"
+ href="${request.application_url}/static/style.css" />
+</head>
+
+<body>
+
+<div class="main_content">
+<div style="float:right; width: 10em;"> Viewing
+<span tal:replace="page.__name__">Page Name Goes Here</span> <br/>
+You can return to the <a href="${request.application_url}">FrontPage</a>.
+ <span tal:condition="logged_in"><a href="${request.application_url}/logout">Logout</a></span>
+</div>
+
+<div tal:replace="structure content">Page text goes here.</div>
+<p><a tal:attributes="href edit_url" href="">Edit this page</a></p>
+</div>
+
+</body></html>
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/tests.py b/docs/tutorials/bfgwiki/src/authorization/tutorial/tests.py
new file mode 100644
index 000000000..84a3a0f37
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/tests.py
@@ -0,0 +1,150 @@
+import unittest
+
+from repoze.bfg import testing
+
+class PageModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Page
+ return Page
+
+ def _makeOne(self, data=u'some data'):
+ return self._getTargetClass()(data=data)
+
+ def test_constructor(self):
+ instance = self._makeOne()
+ self.assertEqual(instance.data, u'some data')
+
+class WikiModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Wiki
+ return Wiki
+
+ def _makeOne(self):
+ return self._getTargetClass()()
+
+ def test_it(self):
+ wiki = self._makeOne()
+ self.assertEqual(wiki.__parent__, None)
+ self.assertEqual(wiki.__name__, None)
+
+class AppmakerTests(unittest.TestCase):
+ def _callFUT(self, zodb_root):
+ from tutorial.models import appmaker
+ return appmaker(zodb_root)
+
+ def test_it(self):
+ root = {}
+ self._callFUT(root)
+ self.assertEqual(root['app_root']['FrontPage'].data,
+ 'This is the front page')
+
+class ViewWikiTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def test_it(self):
+ from tutorial.views import view_wiki
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ response = view_wiki(context, request)
+ self.assertEqual(response.location, 'http://example.com/FrontPage')
+
+class ViewPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import view_page
+ return view_page(context, request)
+
+ def test_it(self):
+ wiki = testing.DummyModel()
+ wiki['IDoExist'] = testing.DummyModel()
+ context = testing.DummyModel(data='Hello CruelWorld IDoExist')
+ context.__parent__ = wiki
+ context.__name__ = 'thepage'
+ request = testing.DummyRequest()
+ renderer = testing.registerDummyRenderer('templates/view.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(
+ renderer.content,
+ '<div class="document">\n'
+ '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
+ 'CruelWorld</a> '
+ '<a href="http://example.com/IDoExist/">'
+ 'IDoExist</a>'
+ '</p>\n</div>\n')
+ self.assertEqual(renderer.edit_url,
+ 'http://example.com/thepage/edit_page')
+
+
+class AddPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import add_page
+ return add_page(context, request)
+
+ def test_it_notsubmitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ request.subpath = ['AnotherPage']
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(renderer.page.data, '')
+
+ def test_it_submitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ request.subpath = ['AnotherPage']
+ response = self._callFUT(context, request)
+ page = context['AnotherPage']
+ self.assertEqual(page.data, 'Hello yo!')
+ self.assertEqual(page.__name__, 'AnotherPage')
+ self.assertEqual(page.__parent__, context)
+
+class EditPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import edit_page
+ return edit_page(context, request)
+
+ def test_it_notsubmitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(renderer.page, context)
+
+ def test_it_submitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(response.location, 'http://example.com/')
+ self.assertEqual(context.data, 'Hello yo!')
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/views.py b/docs/tutorials/bfgwiki/src/authorization/tutorial/views.py
new file mode 100644
index 000000000..7beab58b0
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/views.py
@@ -0,0 +1,102 @@
+from docutils.core import publish_parts
+import re
+
+from webob.exc import HTTPFound
+from repoze.bfg.url import model_url
+from repoze.bfg.chameleon_zpt import render_template_to_response
+from repoze.bfg.view import static
+from repoze.bfg.view import bfg_view
+
+from tutorial.models import Page
+from tutorial.models import Wiki
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+static_app = static('templates/static')
+
+@bfg_view(for_=Wiki, name='static', permission='view')
+def static_view(context, request):
+ return static_app(context, request)
+
+@bfg_view(for_=Wiki, permission='view')
+def view_wiki(context, request):
+ return HTTPFound(location = model_url(context, request, 'FrontPage'))
+
+@bfg_view(for_=Page, permission='view')
+def view_page(context, request):
+ wiki = context.__parent__
+
+ def check(match):
+ word = match.group(1)
+ if word in wiki:
+ page = wiki[word]
+ view_url = model_url(page, request)
+ return '<a href="%s">%s</a>' % (view_url, word)
+ else:
+ add_url = request.application_url + '/add_page/' + word
+ return '<a href="%s">%s</a>' % (add_url, word)
+
+ content = publish_parts(context.data, writer_name='html')['html_body']
+ content = wikiwords.sub(check, content)
+ edit_url = model_url(context, request, 'edit_page')
+
+ logged_in = 'repoze.who.identity' in request.environ
+
+ return render_template_to_response('templates/view.pt',
+ request = request,
+ page = context,
+ content = content,
+ logged_in = logged_in,
+ edit_url = edit_url)
+
+@bfg_view(for_=Wiki, name='add_page', permission='edit')
+def add_page(context, request):
+ name = request.subpath[0]
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(body)
+ page.__name__ = name
+ page.__parent__ = context
+ context[name] = page
+ return HTTPFound(location = model_url(page, request))
+ save_url = model_url(context, request, 'add_page', name)
+ page = Page('')
+ page.__name__ = name
+ page.__parent__ = context
+
+ logged_in = 'repoze.who.identity' in request.environ
+
+ return render_template_to_response('templates/edit.pt',
+ request = request,
+ page = page,
+ logged_in = logged_in,
+ save_url = save_url)
+
+@bfg_view(for_=Page, name='edit_page', permission='edit')
+def edit_page(context, request):
+ if 'form.submitted' in request.params:
+ context.data = request.params['body']
+ return HTTPFound(location = model_url(context, request))
+
+ logged_in = 'repoze.who.identity' in request.environ
+
+ return render_template_to_response('templates/edit.pt',
+ request = request,
+ page = context,
+ logged_in = logged_in,
+ save_url = model_url(context, request,
+ 'edit_page')
+ )
+
+
+@bfg_view(for_=Wiki, name='logout')
+def logout(context, request):
+ identity = request.environ.get('repoze.who.identity')
+ headers = []
+ if identity is not None:
+ auth_tkt = request.environ['repoze.who.plugins']['auth_tkt']
+ headers = auth_tkt.forget(request.environ, identity)
+ return HTTPFound(location = model_url(context, request),
+ headers = headers)
+
diff --git a/docs/tutorials/bfgwiki/src/authorization/who.ini b/docs/tutorials/bfgwiki/src/authorization/who.ini
new file mode 100644
index 000000000..73d820b3d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/who.ini
@@ -0,0 +1,40 @@
+[plugin:form]
+# identification and challenge
+use = repoze.who.plugins.form:make_plugin
+login_form_qs = __do_login
+rememberer_name = auth_tkt
+
+[plugin:auth_tkt]
+# identification
+use = repoze.who.plugins.auth_tkt:make_plugin
+secret = s33kr1t
+cookie_name = oatmeal
+secure = False
+include_ip = False
+
+[plugin:htpasswd]
+# authentication
+use = repoze.who.plugins.htpasswd:make_plugin
+filename = %(here)s/wiki.passwd
+check_fn = repoze.who.plugins.htpasswd:plain_check
+
+[general]
+request_classifier = repoze.who.classifiers:default_request_classifier
+challenge_decider = repoze.who.classifiers:default_challenge_decider
+
+[identifiers]
+# plugin_name;classifier_name:.. or just plugin_name (good for any)
+plugins =
+ form;browser
+ auth_tkt
+
+[authenticators]
+# plugin_name;classifier_name.. or just plugin_name (good for any)
+plugins =
+ htpasswd
+
+[challengers]
+# plugin_name;classifier_name:.. or just plugin_name (good for any)
+plugins =
+ form
+
diff --git a/docs/tutorials/bfgwiki/src/authorization/wiki.passwd b/docs/tutorials/bfgwiki/src/authorization/wiki.passwd
new file mode 100644
index 000000000..c9cd6fe83
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/authorization/wiki.passwd
@@ -0,0 +1 @@
+editor:editor
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/CHANGES.txt b/docs/tutorials/bfgwiki/src/basiclayout/CHANGES.txt
new file mode 100644
index 000000000..1544cf53b
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/CHANGES.txt
@@ -0,0 +1,3 @@
+0.1
+
+ Initial version
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/README.txt b/docs/tutorials/bfgwiki/src/basiclayout/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/ez_setup.py b/docs/tutorials/bfgwiki/src/basiclayout/ez_setup.py
new file mode 100644
index 000000000..d24e845e5
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/ez_setup.py
@@ -0,0 +1,276 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ else:
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+ except pkg_resources.DistributionNotFound:
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/setup.cfg b/docs/tutorials/bfgwiki/src/basiclayout/setup.cfg
new file mode 100644
index 000000000..8ce7ae0fb
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/setup.cfg
@@ -0,0 +1,2 @@
+[easy_install]
+index_url = http://dist.repoze.org/bfg/current/simple
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/setup.py b/docs/tutorials/bfgwiki/src/basiclayout/setup.py
new file mode 100644
index 000000000..6f8b1e1e1
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/setup.py
@@ -0,0 +1,47 @@
+import os
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.txt')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+setup(name='tutorial',
+ version='0.1',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+ "Topic :: Internet :: WWW/HTTP :: WSGI",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg zope',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ 'repoze.bfg',
+ 'repoze.zodbconn',
+ 'repoze.tm',
+ 'ZODB3',
+ ],
+ tests_require=[
+ 'repoze.bfg',
+ ],
+ test_suite="tutorial",
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial.ini b/docs/tutorials/bfgwiki/src/basiclayout/tutorial.ini
new file mode 100644
index 000000000..897a2bde6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+debug = true
+
+[app:zodb]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+[pipeline:main]
+pipeline =
+ egg:repoze.zodbconn#closer
+ egg:repoze.tm#tm
+ zodb
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/__init__.py b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/configure.zcml b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/configure.zcml
new file mode 100644
index 000000000..89bf74525
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/configure.zcml
@@ -0,0 +1,17 @@
+<configure xmlns="http://namespaces.repoze.org/bfg">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="repoze.bfg.includes" />
+
+ <view
+ for=".models.MyModel"
+ view=".views.my_view"
+ />
+
+ <view
+ for=".models.MyModel"
+ view=".views.static_view"
+ name="static"
+ />
+
+</configure>
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/models.py b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/models.py
new file mode 100644
index 000000000..8dd0f5a49
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/models.py
@@ -0,0 +1,12 @@
+from persistent.mapping import PersistentMapping
+
+class MyModel(PersistentMapping):
+ __parent__ = __name__ = None
+
+def appmaker(zodb_root):
+ if not 'app_root' in zodb_root:
+ app_root = MyModel()
+ zodb_root['app_root'] = app_root
+ import transaction
+ transaction.commit()
+ return zodb_root['app_root']
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/run.py b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/run.py
new file mode 100644
index 000000000..89953e6de
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/run.py
@@ -0,0 +1,17 @@
+from repoze.bfg.router import make_app
+from repoze.zodbconn.finder import PersistentApplicationFinder
+
+def app(global_config, **kw):
+ """ This function returns a repoze.bfg.router.Router object.
+
+ It is usually called by the PasteDeploy framework during ``paster serve``.
+ """
+ # paster app config callback
+ import tutorial
+ from tutorial.models import appmaker
+ zodb_uri = kw.get('zodb_uri')
+ if zodb_uri is None:
+ raise ValueError("No 'zodb_uri' in application configuration.")
+
+ get_root = PersistentApplicationFinder(zodb_uri, appmaker)
+ return make_app(get_root, tutorial, options=kw)
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..767252554
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/mytemplate.pt
@@ -0,0 +1,99 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<title>${project} Application</title>
+<meta name="keywords" content="python web application" />
+<meta name="description" content="repoze.bfg web application" />
+<link href="${request.application_url}/static/default.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+<!-- start header -->
+<div id="logo">
+ <h2><code>${project}</code>, a <code>repoze.bfg</code> application</h2>
+</div>
+<div id="header">
+ <div id="menu">
+ </div>
+</div>
+<!-- end header -->
+<div id="wrapper">
+ <!-- start page -->
+ <div id="page">
+ <!-- start content -->
+ <div id="content">
+ <div class="post">
+ <h1 class="title">Welcome to <code>${project}</code>, an
+ application generated by the <a
+ href="http://bfg.repoze.org">repoze.bfg</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>repoze.bfg</code> Documentation</h2>
+ <form method="get"
+ action="http://bfg.repoze.org/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>repoze.bfg</code> links</h2>
+ <ul>
+ <li><a
+ href="http://docs.repoze.org/bfg/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#repoze">IRC Channel</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <!-- end sidebar -->
+ <div style="clear: both;">&nbsp;</div>
+ </div>
+</div>
+<!-- end page -->
+<!-- start footer -->
+<div id="footer">
+ <p id="legal">( c ) 2008. All Rights Reserved. Template design
+ by <a href="http://www.freecsstemplates.org/">Free CSS
+ Templates</a>.</p>
+</div>
+<!-- end footer -->
+</body>
+</html>
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/default.css b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/default.css
@@ -0,0 +1,380 @@
+/*
+Design by Free CSS Templates
+http://www.freecsstemplates.org
+Released for free under a Creative Commons Attribution 2.5 License
+*/
+
+body {
+ margin: 0;
+ padding: 0;
+ background: url(images/img01.gif) repeat-x left top;
+ font-size: 13px;
+ font-family: "Trebuchet MS", Georgia, "Times New Roman", Times, serif;
+ text-align: justify;
+ color: #FFFFFF;
+}
+
+h1, h2, h3 {
+ margin: 0;
+ text-transform: lowercase;
+ font-weight: normal;
+ color: #FFFFFF;
+}
+
+h1 {
+ letter-spacing: -1px;
+ font-size: 32px;
+}
+
+h2 {
+ font-size: 23px;
+}
+
+p, ul, ol {
+ margin: 0 0 2em 0;
+ text-align: justify;
+ line-height: 26px;
+}
+
+a:link {
+ color: #8BD80E;
+}
+
+a:hover, a:active {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+a:visited {
+ color: #8BD80E;
+}
+
+img {
+ border: none;
+}
+
+img.left {
+ float: left;
+ margin-right: 15px;
+}
+
+img.right {
+ float: right;
+ margin-left: 15px;
+}
+
+/* Form */
+
+form {
+ margin: 0;
+ padding: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+legend {
+ display: none;
+}
+
+input, textarea, select {
+ font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
+ font-size: 13px;
+ color: #333333;
+}
+
+#wrapper {
+ margin: 0;
+ padding: 0;
+ background: #000000;
+}
+
+/* Header */
+
+#header {
+ width: 713px;
+ margin: 0 auto;
+ height: 42px;
+}
+
+/* Menu */
+
+#menu {
+ float: left;
+ width: 713px;
+ height: 50px;
+ background: url(images/img02.gif) no-repeat left top;
+}
+
+#menu ul {
+ margin: 0;
+ padding: 0px 0 0 10px;
+ list-style: none;
+ line-height: normal;
+}
+
+#menu li {
+ display: block;
+ float: left;
+}
+
+#menu a {
+ display: block;
+ float: left;
+ background: url(images/img04.gif) no-repeat right 55%;
+ margin-top: 5px;
+ margin-right: 3px;
+ padding: 8px 17px;
+ text-decoration: none;
+ font-size: 13px;
+ color: #000000;
+}
+
+#menu a:hover {
+ color: #000000;
+}
+
+#menu .current_page_item a {
+ color: #000000;
+}
+
+/** LOGO */
+
+#logo {
+ width: 713px;
+ height: 80px;
+ margin: 0 auto;
+}
+
+#logo h1, #logo h2 {
+ float: left;
+ margin: 0;
+ padding: 30px 0 0 0px;
+ line-height: normal;
+}
+
+#logo h1 {
+ font-family: Georgia, "Times New Roman", Times, serif;
+ font-size:40px;
+}
+
+#logo h1 a {
+ text-decoration: none;
+ color: #4C4C4C;
+}
+
+#logo h1 a:hover { text-decoration: underline; }
+
+#logo h2 {
+ float: left;
+ padding: 45px 0 0 18px;
+ font: 18px Georgia, "Times New Roman", Times, serif;
+ color: #8BD80E;
+}
+
+#logo p a {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+#logo p a:hover { text-decoration: underline; }
+
+
+
+/* Page */
+
+#page {
+ width: 663px;
+ margin: 0 auto;
+ background: #4C4C4C url(images/img03.gif) no-repeat left bottom;
+ padding: 0 25px;
+}
+
+/* Content */
+
+#content {
+ float: left;
+ width: 410px;
+
+}
+
+/* Post */
+
+.post {
+ padding: 15px 0px;
+ margin-bottom: 20px;
+}
+
+.post .title {
+ margin-bottom: 20px;
+ padding-bottom: 5px;
+}
+
+.post h1 {
+ padding: 0px 0 0 0px;
+ background: url(images/img08.jpg) no-repeat left top;
+ font-size: 24px;
+ color: #FFFFFF;
+}
+
+.post h2 {
+ padding: 0px 0 0 0px;
+ font-size: 22px;
+ color: #FFFFFF;
+}
+
+.post .entry {
+}
+
+.post .meta {
+ padding: 15px 15px 30px 0px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+}
+
+.post .meta p {
+ margin: 0;
+ padding-top: 15px;
+ line-height: normal;
+ color: #FFFFFF;
+}
+
+.post .meta .byline {
+ float: left;
+}
+
+.post .meta .links {
+ float: right;
+}
+
+.post .meta .more {
+ padding: 0 10px 0 18px;
+}
+
+.post .meta .comments {
+}
+
+.post .meta b {
+ display: none;
+}
+
+
+/* Sidebar */
+
+#sidebar {
+ width: 210px;
+ float: right;
+ margin: 0;
+ padding: 0;
+}
+
+#sidebar ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+#sidebar li {
+ margin-bottom: 40px;
+}
+
+#sidebar li ul {
+}
+
+#sidebar li li {
+ margin: 0;
+}
+
+#sidebar h2 {
+ width: 250px;
+ padding: 8px 0 0 0px;
+ margin-bottom: 10px;
+ background: url(images/img07.jpg) no-repeat left top;
+ font-size: 20px;
+ color: #FFFFFF;
+}
+
+/* Search */
+
+#search {
+
+}
+
+#search h2 {
+ margin-bottom: 20px;
+}
+
+#s {
+ width: 140px;
+ margin-right: 5px;
+ padding: 3px;
+ border: 1px solid #BED99C;
+}
+
+#x {
+ padding: 3px;
+ border: none;
+ background: #8BD80E;
+ text-transform: lowercase;
+ font-size: 11px;
+ color: #FFFFFF;
+}
+
+/* Boxes */
+
+.box1 {
+ padding: 20px;
+}
+
+.box2 {
+ color: #BABABA;
+}
+
+.box2 h2 {
+ margin-bottom: 15px;
+ font-size: 16px;
+ color: #FFFFFF;
+}
+
+.box2 ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.box2 a:link, .box2 a:hover, .box2 a:active, .box2 a:visited {
+ color: #EDEDED;
+}
+
+/* Footer */
+#footer-wrap {
+}
+
+#footer {
+ margin: 0 auto;
+ padding: 20px 0 10px 0;
+ background: #000000;
+}
+
+html>body #footer {
+ height: auto;
+}
+
+#footer p {
+ font-size: 11px;
+}
+
+#legal {
+ clear: both;
+ padding-top: 17px;
+ text-align: center;
+ color: #FFFFFF;
+}
+
+#legal a {
+ font-weight: normal;
+ color: #FFFFFF;
+}
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img01.gif b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img02.gif b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img03.gif b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img04.gif b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/spacer.gif b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/templatelicense.txt b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/templates/static/templatelicense.txt
@@ -0,0 +1,243 @@
+Creative Commons </>
+
+Creative Commons Legal Code
+
+*Attribution 2.5*
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
+ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE
+INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ITS USE.
+
+/License/
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE
+RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS
+AND CONDITIONS.
+
+*1. Definitions*
+
+ 1. *"Collective Work"* means a work, such as a periodical issue,
+ anthology or encyclopedia, in which the Work in its entirety in
+ unmodified form, along with a number of other contributions,
+ constituting separate and independent works in themselves, are
+ assembled into a collective whole. A work that constitutes a
+ Collective Work will not be considered a Derivative Work (as
+ defined below) for the purposes of this License.
+ 2. *"Derivative Work"* means a work based upon the Work or upon the
+ Work and other pre-existing works, such as a translation, musical
+ arrangement, dramatization, fictionalization, motion picture
+ version, sound recording, art reproduction, abridgment,
+ condensation, or any other form in which the Work may be recast,
+ transformed, or adapted, except that a work that constitutes a
+ Collective Work will not be considered a Derivative Work for the
+ purpose of this License. For the avoidance of doubt, where the
+ Work is a musical composition or sound recording, the
+ synchronization of the Work in timed-relation with a moving image
+ ("synching") will be considered a Derivative Work for the purpose
+ of this License.
+ 3. *"Licensor"* means the individual or entity that offers the Work
+ under the terms of this License.
+ 4. *"Original Author"* means the individual or entity who created the
+ Work.
+ 5. *"Work"* means the copyrightable work of authorship offered under
+ the terms of this License.
+ 6. *"You"* means an individual or entity exercising rights under this
+ License who has not previously violated the terms of this License
+ with respect to the Work, or who has received express permission
+ from the Licensor to exercise rights under this License despite a
+ previous violation.
+
+*2. Fair Use Rights.* Nothing in this license is intended to reduce,
+limit, or restrict any rights arising from fair use, first sale or other
+limitations on the exclusive rights of the copyright owner under
+copyright law or other applicable laws.
+
+*3. License Grant.* Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ 1. to reproduce the Work, to incorporate the Work into one or more
+ Collective Works, and to reproduce the Work as incorporated in the
+ Collective Works;
+ 2. to create and reproduce Derivative Works;
+ 3. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission the Work including as incorporated in Collective Works;
+ 4. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission Derivative Works.
+ 5.
+
+ For the avoidance of doubt, where the work is a musical composition:
+
+ 1. *Performance Royalties Under Blanket Licenses*. Licensor
+ waives the exclusive right to collect, whether individually
+ or via a performance rights society (e.g. ASCAP, BMI,
+ SESAC), royalties for the public performance or public
+ digital performance (e.g. webcast) of the Work.
+ 2. *Mechanical Rights and Statutory Royalties*. Licensor waives
+ the exclusive right to collect, whether individually or via
+ a music rights agency or designated agent (e.g. Harry Fox
+ Agency), royalties for any phonorecord You create from the
+ Work ("cover version") and distribute, subject to the
+ compulsory license created by 17 USC Section 115 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+ 6. *Webcasting Rights and Statutory Royalties*. For the avoidance of
+ doubt, where the Work is a sound recording, Licensor waives the
+ exclusive right to collect, whether individually or via a
+ performance-rights society (e.g. SoundExchange), royalties for the
+ public digital performance (e.g. webcast) of the Work, subject to
+ the compulsory license created by 17 USC Section 114 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights
+in other media and formats. All rights not expressly granted by Licensor
+are hereby reserved.
+
+*4. Restrictions.*The license granted in Section 3 above is expressly
+made subject to and limited by the following restrictions:
+
+ 1. You may distribute, publicly display, publicly perform, or
+ publicly digitally perform the Work only under the terms of this
+ License, and You must include a copy of, or the Uniform Resource
+ Identifier for, this License with every copy or phonorecord of the
+ Work You distribute, publicly display, publicly perform, or
+ publicly digitally perform. You may not offer or impose any terms
+ on the Work that alter or restrict the terms of this License or
+ the recipients' exercise of the rights granted hereunder. You may
+ not sublicense the Work. You must keep intact all notices that
+ refer to this License and to the disclaimer of warranties. You may
+ not distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work with any technological measures that
+ control access or use of the Work in a manner inconsistent with
+ the terms of this License Agreement. The above applies to the Work
+ as incorporated in a Collective Work, but this does not require
+ the Collective Work apart from the Work itself to be made subject
+ to the terms of this License. If You create a Collective Work,
+ upon notice from any Licensor You must, to the extent practicable,
+ remove from the Collective Work any credit as required by clause
+ 4(b), as requested. If You create a Derivative Work, upon notice
+ from any Licensor You must, to the extent practicable, remove from
+ the Derivative Work any credit as required by clause 4(b), as
+ requested.
+ 2. If you distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work or any Derivative Works or Collective
+ Works, You must keep intact all copyright notices for the Work and
+ provide, reasonable to the medium or means You are utilizing: (i)
+ the name of the Original Author (or pseudonym, if applicable) if
+ supplied, and/or (ii) if the Original Author and/or Licensor
+ designate another party or parties (e.g. a sponsor institute,
+ publishing entity, journal) for attribution in Licensor's
+ copyright notice, terms of service or by other reasonable means,
+ the name of such party or parties; the title of the Work if
+ supplied; to the extent reasonably practicable, the Uniform
+ Resource Identifier, if any, that Licensor specifies to be
+ associated with the Work, unless such URI does not refer to the
+ copyright notice or licensing information for the Work; and in the
+ case of a Derivative Work, a credit identifying the use of the
+ Work in the Derivative Work (e.g., "French translation of the Work
+ by Original Author," or "Screenplay based on original Work by
+ Original Author"). Such credit may be implemented in any
+ reasonable manner; provided, however, that in the case of a
+ Derivative Work or Collective Work, at a minimum such credit will
+ appear where any other comparable authorship credit appears and in
+ a manner at least as prominent as such other comparable authorship
+ credit.
+
+*5. Representations, Warranties and Disclaimer*
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE
+EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+*6. Limitation on Liability.* EXCEPT TO THE EXTENT REQUIRED BY
+APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL
+THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY
+DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF
+LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+*7. Termination*
+
+ 1. This License and the rights granted hereunder will terminate
+ automatically upon any breach by You of the terms of this License.
+ Individuals or entities who have received Derivative Works or
+ Collective Works from You under this License, however, will not
+ have their licenses terminated provided such individuals or
+ entities remain in full compliance with those licenses. Sections
+ 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
+ 2. Subject to the above terms and conditions, the license granted
+ here is perpetual (for the duration of the applicable copyright in
+ the Work). Notwithstanding the above, Licensor reserves the right
+ to release the Work under different license terms or to stop
+ distributing the Work at any time; provided, however that any such
+ election will not serve to withdraw this License (or any other
+ license that has been, or is required to be, granted under the
+ terms of this License), and this License will continue in full
+ force and effect unless terminated as stated above.
+
+*8. Miscellaneous*
+
+ 1. Each time You distribute or publicly digitally perform the Work or
+ a Collective Work, the Licensor offers to the recipient a license
+ to the Work on the same terms and conditions as the license
+ granted to You under this License.
+ 2. Each time You distribute or publicly digitally perform a
+ Derivative Work, Licensor offers to the recipient a license to the
+ original Work on the same terms and conditions as the license
+ granted to You under this License.
+ 3. If any provision of this License is invalid or unenforceable under
+ applicable law, it shall not affect the validity or enforceability
+ of the remainder of the terms of this License, and without further
+ action by the parties to this agreement, such provision shall be
+ reformed to the minimum extent necessary to make such provision
+ valid and enforceable.
+ 4. No term or provision of this License shall be deemed waived and no
+ breach consented to unless such waiver or consent shall be in
+ writing and signed by the party to be charged with such waiver or
+ consent.
+ 5. This License constitutes the entire agreement between the parties
+ with respect to the Work licensed here. There are no
+ understandings, agreements or representations with respect to the
+ Work not specified here. Licensor shall not be bound by any
+ additional provisions that may appear in any communication from
+ You. This License may not be modified without the mutual written
+ agreement of the Licensor and You.
+
+Creative Commons is not a party to this License, and makes no warranty
+whatsoever in connection with the Work. Creative Commons will not be
+liable to You or any party on any legal theory for any damages
+whatsoever, including without limitation any general, special,
+incidental or consequential damages arising in connection to this
+license. Notwithstanding the foregoing two (2) sentences, if Creative
+Commons has expressly identified itself as the Licensor hereunder, it
+shall have all rights and obligations of Licensor.
+
+Except for the limited purpose of indicating to the public that the Work
+is licensed under the CCPL, neither party will use the trademark
+"Creative Commons" or any related trademark or logo of Creative Commons
+without the prior written consent of Creative Commons. Any permitted use
+will be in compliance with Creative Commons' then-current trademark
+usage guidelines, as may be published on its website or otherwise made
+available upon request from time to time.
+
+Creative Commons may be contacted at http://creativecommons.org/
+<http://creativecommons.org>.
+
+« Back to Commons Deed <./>
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/tests.py b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/tests.py
new file mode 100644
index 000000000..408645fcb
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/tests.py
@@ -0,0 +1,73 @@
+import unittest
+
+from repoze.bfg import testing
+
+class ViewTests(unittest.TestCase):
+
+ """ These tests are unit tests for the view. They test the
+ functionality of *only* the view. They register and use dummy
+ implementations of repoze.bfg functionality to allow you to avoid
+ testing 'too much'"""
+
+ def setUp(self):
+ """ cleanUp() is required to clear out the application registry
+ between tests (done in setUp for good measure too)
+ """
+ testing.cleanUp()
+
+ def tearDown(self):
+ """ cleanUp() is required to clear out the application registry
+ between tests
+ """
+ testing.cleanUp()
+
+ def test_my_view(self):
+ from tutorial.views import my_view
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ renderer = testing.registerDummyRenderer('templates/mytemplate.pt')
+ response = my_view(context, request)
+ renderer.assert_(project='tutorial')
+
+class ViewIntegrationTests(unittest.TestCase):
+ """ These tests are integration tests for the view. These test
+ the functionality the view *and* its integration with the rest of
+ the repoze.bfg framework. They cause the entire environment to be
+ set up and torn down as if your application was running 'for
+ real'. This is a heavy-hammer way of making sure that your tests
+ have enough context to run properly, and it tests your view's
+ integration with the rest of BFG. You should not use this style
+ of test to perform 'true' unit testing as tests will run faster
+ and will be easier to write if you use the testing facilities
+ provided by bfg and only the registrations you need, as in the
+ above ViewTests.
+ """
+ def setUp(self):
+ """ This sets up the application registry with the
+ registrations your application declares in its configure.zcml
+ (including dependent registrations for repoze.bfg itself).
+ """
+ testing.cleanUp()
+ import tutorial
+ import zope.configuration.xmlconfig
+ zope.configuration.xmlconfig.file('configure.zcml',
+ package=tutorial)
+
+ def tearDown(self):
+ """ Clear out the application registry """
+ testing.cleanUp()
+
+ def test_my_view(self):
+ from tutorial.views import my_view
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ result = my_view(context, request)
+ self.assertEqual(result.status, '200 OK')
+ body = result.app_iter[0]
+ self.failUnless('Welcome to' in body)
+ self.assertEqual(len(result.headerlist), 2)
+ self.assertEqual(result.headerlist[0],
+ ('Content-Type', 'text/html; charset=UTF-8'))
+ self.assertEqual(result.headerlist[1], ('Content-Length',
+ str(len(body))))
+
diff --git a/docs/tutorials/bfgwiki/src/basiclayout/tutorial/views.py b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/views.py
new file mode 100644
index 000000000..13b84352f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/basiclayout/tutorial/views.py
@@ -0,0 +1,9 @@
+from repoze.bfg.chameleon_zpt import render_template_to_response
+from repoze.bfg.view import static
+
+static_view = static('templates/static')
+
+def my_view(context, request):
+ return render_template_to_response('templates/mytemplate.pt',
+ request = request,
+ project = 'tutorial')
diff --git a/docs/tutorials/bfgwiki/src/models/CHANGES.txt b/docs/tutorials/bfgwiki/src/models/CHANGES.txt
new file mode 100644
index 000000000..1544cf53b
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/CHANGES.txt
@@ -0,0 +1,3 @@
+0.1
+
+ Initial version
diff --git a/docs/tutorials/bfgwiki/src/models/README.txt b/docs/tutorials/bfgwiki/src/models/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/models/ez_setup.py b/docs/tutorials/bfgwiki/src/models/ez_setup.py
new file mode 100644
index 000000000..d24e845e5
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/ez_setup.py
@@ -0,0 +1,276 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ else:
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+ except pkg_resources.DistributionNotFound:
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/models/setup.cfg b/docs/tutorials/bfgwiki/src/models/setup.cfg
new file mode 100644
index 000000000..8ce7ae0fb
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/setup.cfg
@@ -0,0 +1,2 @@
+[easy_install]
+index_url = http://dist.repoze.org/bfg/current/simple
diff --git a/docs/tutorials/bfgwiki/src/models/setup.py b/docs/tutorials/bfgwiki/src/models/setup.py
new file mode 100644
index 000000000..b289ca8b0
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/setup.py
@@ -0,0 +1,48 @@
+import os
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.txt')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+requires = [
+ 'repoze.bfg',
+ 'docutils',
+ 'ZODB3',
+ 'repoze.zodbconn',
+ 'repoze.tm',
+ ]
+
+setup(name='tutorial',
+ version='0.1',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+ "Topic :: Internet :: WWW/HTTP :: WSGI",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg zope',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=requires,
+ tests_require=requires,
+ test_suite="tutorial",
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial.ini b/docs/tutorials/bfgwiki/src/models/tutorial.ini
new file mode 100644
index 000000000..897a2bde6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+debug = true
+
+[app:zodb]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+[pipeline:main]
+pipeline =
+ egg:repoze.zodbconn#closer
+ egg:repoze.tm#tm
+ zodb
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/__init__.py b/docs/tutorials/bfgwiki/src/models/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/configure.zcml b/docs/tutorials/bfgwiki/src/models/tutorial/configure.zcml
new file mode 100644
index 000000000..c734bb61f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/configure.zcml
@@ -0,0 +1,17 @@
+<configure xmlns="http://namespaces.repoze.org/bfg">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="repoze.bfg.includes" />
+
+ <view
+ for=".models.Wiki"
+ view=".views.my_view"
+ />
+
+ <view
+ for=".models.Wiki"
+ view=".views.static_view"
+ name="static"
+ />
+
+</configure>
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/models.py b/docs/tutorials/bfgwiki/src/models/tutorial/models.py
new file mode 100644
index 000000000..9761856c6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/models.py
@@ -0,0 +1,22 @@
+from persistent import Persistent
+from persistent.mapping import PersistentMapping
+
+class Wiki(PersistentMapping):
+ __name__ = None
+ __parent__ = None
+
+class Page(Persistent):
+ def __init__(self, data):
+ self.data = data
+
+def appmaker(zodb_root):
+ if not 'app_root' in zodb_root:
+ app_root = Wiki()
+ frontpage = Page('This is the front page')
+ app_root['FrontPage'] = frontpage
+ frontpage.__name__ = 'FrontPage'
+ frontpage.__parent__ = app_root
+ zodb_root['app_root'] = app_root
+ import transaction
+ transaction.commit()
+ return zodb_root['app_root']
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/run.py b/docs/tutorials/bfgwiki/src/models/tutorial/run.py
new file mode 100644
index 000000000..89953e6de
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/run.py
@@ -0,0 +1,17 @@
+from repoze.bfg.router import make_app
+from repoze.zodbconn.finder import PersistentApplicationFinder
+
+def app(global_config, **kw):
+ """ This function returns a repoze.bfg.router.Router object.
+
+ It is usually called by the PasteDeploy framework during ``paster serve``.
+ """
+ # paster app config callback
+ import tutorial
+ from tutorial.models import appmaker
+ zodb_uri = kw.get('zodb_uri')
+ if zodb_uri is None:
+ raise ValueError("No 'zodb_uri' in application configuration.")
+
+ get_root = PersistentApplicationFinder(zodb_uri, appmaker)
+ return make_app(get_root, tutorial, options=kw)
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/bfgwiki/src/models/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..767252554
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/templates/mytemplate.pt
@@ -0,0 +1,99 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<title>${project} Application</title>
+<meta name="keywords" content="python web application" />
+<meta name="description" content="repoze.bfg web application" />
+<link href="${request.application_url}/static/default.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+<!-- start header -->
+<div id="logo">
+ <h2><code>${project}</code>, a <code>repoze.bfg</code> application</h2>
+</div>
+<div id="header">
+ <div id="menu">
+ </div>
+</div>
+<!-- end header -->
+<div id="wrapper">
+ <!-- start page -->
+ <div id="page">
+ <!-- start content -->
+ <div id="content">
+ <div class="post">
+ <h1 class="title">Welcome to <code>${project}</code>, an
+ application generated by the <a
+ href="http://bfg.repoze.org">repoze.bfg</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>repoze.bfg</code> Documentation</h2>
+ <form method="get"
+ action="http://bfg.repoze.org/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>repoze.bfg</code> links</h2>
+ <ul>
+ <li><a
+ href="http://docs.repoze.org/bfg/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#repoze">IRC Channel</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <!-- end sidebar -->
+ <div style="clear: both;">&nbsp;</div>
+ </div>
+</div>
+<!-- end page -->
+<!-- start footer -->
+<div id="footer">
+ <p id="legal">( c ) 2008. All Rights Reserved. Template design
+ by <a href="http://www.freecsstemplates.org/">Free CSS
+ Templates</a>.</p>
+</div>
+<!-- end footer -->
+</body>
+</html>
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/default.css b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/default.css
@@ -0,0 +1,380 @@
+/*
+Design by Free CSS Templates
+http://www.freecsstemplates.org
+Released for free under a Creative Commons Attribution 2.5 License
+*/
+
+body {
+ margin: 0;
+ padding: 0;
+ background: url(images/img01.gif) repeat-x left top;
+ font-size: 13px;
+ font-family: "Trebuchet MS", Georgia, "Times New Roman", Times, serif;
+ text-align: justify;
+ color: #FFFFFF;
+}
+
+h1, h2, h3 {
+ margin: 0;
+ text-transform: lowercase;
+ font-weight: normal;
+ color: #FFFFFF;
+}
+
+h1 {
+ letter-spacing: -1px;
+ font-size: 32px;
+}
+
+h2 {
+ font-size: 23px;
+}
+
+p, ul, ol {
+ margin: 0 0 2em 0;
+ text-align: justify;
+ line-height: 26px;
+}
+
+a:link {
+ color: #8BD80E;
+}
+
+a:hover, a:active {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+a:visited {
+ color: #8BD80E;
+}
+
+img {
+ border: none;
+}
+
+img.left {
+ float: left;
+ margin-right: 15px;
+}
+
+img.right {
+ float: right;
+ margin-left: 15px;
+}
+
+/* Form */
+
+form {
+ margin: 0;
+ padding: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+legend {
+ display: none;
+}
+
+input, textarea, select {
+ font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
+ font-size: 13px;
+ color: #333333;
+}
+
+#wrapper {
+ margin: 0;
+ padding: 0;
+ background: #000000;
+}
+
+/* Header */
+
+#header {
+ width: 713px;
+ margin: 0 auto;
+ height: 42px;
+}
+
+/* Menu */
+
+#menu {
+ float: left;
+ width: 713px;
+ height: 50px;
+ background: url(images/img02.gif) no-repeat left top;
+}
+
+#menu ul {
+ margin: 0;
+ padding: 0px 0 0 10px;
+ list-style: none;
+ line-height: normal;
+}
+
+#menu li {
+ display: block;
+ float: left;
+}
+
+#menu a {
+ display: block;
+ float: left;
+ background: url(images/img04.gif) no-repeat right 55%;
+ margin-top: 5px;
+ margin-right: 3px;
+ padding: 8px 17px;
+ text-decoration: none;
+ font-size: 13px;
+ color: #000000;
+}
+
+#menu a:hover {
+ color: #000000;
+}
+
+#menu .current_page_item a {
+ color: #000000;
+}
+
+/** LOGO */
+
+#logo {
+ width: 713px;
+ height: 80px;
+ margin: 0 auto;
+}
+
+#logo h1, #logo h2 {
+ float: left;
+ margin: 0;
+ padding: 30px 0 0 0px;
+ line-height: normal;
+}
+
+#logo h1 {
+ font-family: Georgia, "Times New Roman", Times, serif;
+ font-size:40px;
+}
+
+#logo h1 a {
+ text-decoration: none;
+ color: #4C4C4C;
+}
+
+#logo h1 a:hover { text-decoration: underline; }
+
+#logo h2 {
+ float: left;
+ padding: 45px 0 0 18px;
+ font: 18px Georgia, "Times New Roman", Times, serif;
+ color: #8BD80E;
+}
+
+#logo p a {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+#logo p a:hover { text-decoration: underline; }
+
+
+
+/* Page */
+
+#page {
+ width: 663px;
+ margin: 0 auto;
+ background: #4C4C4C url(images/img03.gif) no-repeat left bottom;
+ padding: 0 25px;
+}
+
+/* Content */
+
+#content {
+ float: left;
+ width: 410px;
+
+}
+
+/* Post */
+
+.post {
+ padding: 15px 0px;
+ margin-bottom: 20px;
+}
+
+.post .title {
+ margin-bottom: 20px;
+ padding-bottom: 5px;
+}
+
+.post h1 {
+ padding: 0px 0 0 0px;
+ background: url(images/img08.jpg) no-repeat left top;
+ font-size: 24px;
+ color: #FFFFFF;
+}
+
+.post h2 {
+ padding: 0px 0 0 0px;
+ font-size: 22px;
+ color: #FFFFFF;
+}
+
+.post .entry {
+}
+
+.post .meta {
+ padding: 15px 15px 30px 0px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+}
+
+.post .meta p {
+ margin: 0;
+ padding-top: 15px;
+ line-height: normal;
+ color: #FFFFFF;
+}
+
+.post .meta .byline {
+ float: left;
+}
+
+.post .meta .links {
+ float: right;
+}
+
+.post .meta .more {
+ padding: 0 10px 0 18px;
+}
+
+.post .meta .comments {
+}
+
+.post .meta b {
+ display: none;
+}
+
+
+/* Sidebar */
+
+#sidebar {
+ width: 210px;
+ float: right;
+ margin: 0;
+ padding: 0;
+}
+
+#sidebar ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+#sidebar li {
+ margin-bottom: 40px;
+}
+
+#sidebar li ul {
+}
+
+#sidebar li li {
+ margin: 0;
+}
+
+#sidebar h2 {
+ width: 250px;
+ padding: 8px 0 0 0px;
+ margin-bottom: 10px;
+ background: url(images/img07.jpg) no-repeat left top;
+ font-size: 20px;
+ color: #FFFFFF;
+}
+
+/* Search */
+
+#search {
+
+}
+
+#search h2 {
+ margin-bottom: 20px;
+}
+
+#s {
+ width: 140px;
+ margin-right: 5px;
+ padding: 3px;
+ border: 1px solid #BED99C;
+}
+
+#x {
+ padding: 3px;
+ border: none;
+ background: #8BD80E;
+ text-transform: lowercase;
+ font-size: 11px;
+ color: #FFFFFF;
+}
+
+/* Boxes */
+
+.box1 {
+ padding: 20px;
+}
+
+.box2 {
+ color: #BABABA;
+}
+
+.box2 h2 {
+ margin-bottom: 15px;
+ font-size: 16px;
+ color: #FFFFFF;
+}
+
+.box2 ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.box2 a:link, .box2 a:hover, .box2 a:active, .box2 a:visited {
+ color: #EDEDED;
+}
+
+/* Footer */
+#footer-wrap {
+}
+
+#footer {
+ margin: 0 auto;
+ padding: 20px 0 10px 0;
+ background: #000000;
+}
+
+html>body #footer {
+ height: auto;
+}
+
+#footer p {
+ font-size: 11px;
+}
+
+#legal {
+ clear: both;
+ padding-top: 17px;
+ text-align: center;
+ color: #FFFFFF;
+}
+
+#legal a {
+ font-weight: normal;
+ color: #FFFFFF;
+}
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img01.gif b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img02.gif b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img03.gif b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img04.gif b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/spacer.gif b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/templatelicense.txt b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/templates/static/templatelicense.txt
@@ -0,0 +1,243 @@
+Creative Commons </>
+
+Creative Commons Legal Code
+
+*Attribution 2.5*
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
+ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE
+INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ITS USE.
+
+/License/
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE
+RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS
+AND CONDITIONS.
+
+*1. Definitions*
+
+ 1. *"Collective Work"* means a work, such as a periodical issue,
+ anthology or encyclopedia, in which the Work in its entirety in
+ unmodified form, along with a number of other contributions,
+ constituting separate and independent works in themselves, are
+ assembled into a collective whole. A work that constitutes a
+ Collective Work will not be considered a Derivative Work (as
+ defined below) for the purposes of this License.
+ 2. *"Derivative Work"* means a work based upon the Work or upon the
+ Work and other pre-existing works, such as a translation, musical
+ arrangement, dramatization, fictionalization, motion picture
+ version, sound recording, art reproduction, abridgment,
+ condensation, or any other form in which the Work may be recast,
+ transformed, or adapted, except that a work that constitutes a
+ Collective Work will not be considered a Derivative Work for the
+ purpose of this License. For the avoidance of doubt, where the
+ Work is a musical composition or sound recording, the
+ synchronization of the Work in timed-relation with a moving image
+ ("synching") will be considered a Derivative Work for the purpose
+ of this License.
+ 3. *"Licensor"* means the individual or entity that offers the Work
+ under the terms of this License.
+ 4. *"Original Author"* means the individual or entity who created the
+ Work.
+ 5. *"Work"* means the copyrightable work of authorship offered under
+ the terms of this License.
+ 6. *"You"* means an individual or entity exercising rights under this
+ License who has not previously violated the terms of this License
+ with respect to the Work, or who has received express permission
+ from the Licensor to exercise rights under this License despite a
+ previous violation.
+
+*2. Fair Use Rights.* Nothing in this license is intended to reduce,
+limit, or restrict any rights arising from fair use, first sale or other
+limitations on the exclusive rights of the copyright owner under
+copyright law or other applicable laws.
+
+*3. License Grant.* Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ 1. to reproduce the Work, to incorporate the Work into one or more
+ Collective Works, and to reproduce the Work as incorporated in the
+ Collective Works;
+ 2. to create and reproduce Derivative Works;
+ 3. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission the Work including as incorporated in Collective Works;
+ 4. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission Derivative Works.
+ 5.
+
+ For the avoidance of doubt, where the work is a musical composition:
+
+ 1. *Performance Royalties Under Blanket Licenses*. Licensor
+ waives the exclusive right to collect, whether individually
+ or via a performance rights society (e.g. ASCAP, BMI,
+ SESAC), royalties for the public performance or public
+ digital performance (e.g. webcast) of the Work.
+ 2. *Mechanical Rights and Statutory Royalties*. Licensor waives
+ the exclusive right to collect, whether individually or via
+ a music rights agency or designated agent (e.g. Harry Fox
+ Agency), royalties for any phonorecord You create from the
+ Work ("cover version") and distribute, subject to the
+ compulsory license created by 17 USC Section 115 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+ 6. *Webcasting Rights and Statutory Royalties*. For the avoidance of
+ doubt, where the Work is a sound recording, Licensor waives the
+ exclusive right to collect, whether individually or via a
+ performance-rights society (e.g. SoundExchange), royalties for the
+ public digital performance (e.g. webcast) of the Work, subject to
+ the compulsory license created by 17 USC Section 114 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights
+in other media and formats. All rights not expressly granted by Licensor
+are hereby reserved.
+
+*4. Restrictions.*The license granted in Section 3 above is expressly
+made subject to and limited by the following restrictions:
+
+ 1. You may distribute, publicly display, publicly perform, or
+ publicly digitally perform the Work only under the terms of this
+ License, and You must include a copy of, or the Uniform Resource
+ Identifier for, this License with every copy or phonorecord of the
+ Work You distribute, publicly display, publicly perform, or
+ publicly digitally perform. You may not offer or impose any terms
+ on the Work that alter or restrict the terms of this License or
+ the recipients' exercise of the rights granted hereunder. You may
+ not sublicense the Work. You must keep intact all notices that
+ refer to this License and to the disclaimer of warranties. You may
+ not distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work with any technological measures that
+ control access or use of the Work in a manner inconsistent with
+ the terms of this License Agreement. The above applies to the Work
+ as incorporated in a Collective Work, but this does not require
+ the Collective Work apart from the Work itself to be made subject
+ to the terms of this License. If You create a Collective Work,
+ upon notice from any Licensor You must, to the extent practicable,
+ remove from the Collective Work any credit as required by clause
+ 4(b), as requested. If You create a Derivative Work, upon notice
+ from any Licensor You must, to the extent practicable, remove from
+ the Derivative Work any credit as required by clause 4(b), as
+ requested.
+ 2. If you distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work or any Derivative Works or Collective
+ Works, You must keep intact all copyright notices for the Work and
+ provide, reasonable to the medium or means You are utilizing: (i)
+ the name of the Original Author (or pseudonym, if applicable) if
+ supplied, and/or (ii) if the Original Author and/or Licensor
+ designate another party or parties (e.g. a sponsor institute,
+ publishing entity, journal) for attribution in Licensor's
+ copyright notice, terms of service or by other reasonable means,
+ the name of such party or parties; the title of the Work if
+ supplied; to the extent reasonably practicable, the Uniform
+ Resource Identifier, if any, that Licensor specifies to be
+ associated with the Work, unless such URI does not refer to the
+ copyright notice or licensing information for the Work; and in the
+ case of a Derivative Work, a credit identifying the use of the
+ Work in the Derivative Work (e.g., "French translation of the Work
+ by Original Author," or "Screenplay based on original Work by
+ Original Author"). Such credit may be implemented in any
+ reasonable manner; provided, however, that in the case of a
+ Derivative Work or Collective Work, at a minimum such credit will
+ appear where any other comparable authorship credit appears and in
+ a manner at least as prominent as such other comparable authorship
+ credit.
+
+*5. Representations, Warranties and Disclaimer*
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE
+EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+*6. Limitation on Liability.* EXCEPT TO THE EXTENT REQUIRED BY
+APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL
+THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY
+DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF
+LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+*7. Termination*
+
+ 1. This License and the rights granted hereunder will terminate
+ automatically upon any breach by You of the terms of this License.
+ Individuals or entities who have received Derivative Works or
+ Collective Works from You under this License, however, will not
+ have their licenses terminated provided such individuals or
+ entities remain in full compliance with those licenses. Sections
+ 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
+ 2. Subject to the above terms and conditions, the license granted
+ here is perpetual (for the duration of the applicable copyright in
+ the Work). Notwithstanding the above, Licensor reserves the right
+ to release the Work under different license terms or to stop
+ distributing the Work at any time; provided, however that any such
+ election will not serve to withdraw this License (or any other
+ license that has been, or is required to be, granted under the
+ terms of this License), and this License will continue in full
+ force and effect unless terminated as stated above.
+
+*8. Miscellaneous*
+
+ 1. Each time You distribute or publicly digitally perform the Work or
+ a Collective Work, the Licensor offers to the recipient a license
+ to the Work on the same terms and conditions as the license
+ granted to You under this License.
+ 2. Each time You distribute or publicly digitally perform a
+ Derivative Work, Licensor offers to the recipient a license to the
+ original Work on the same terms and conditions as the license
+ granted to You under this License.
+ 3. If any provision of this License is invalid or unenforceable under
+ applicable law, it shall not affect the validity or enforceability
+ of the remainder of the terms of this License, and without further
+ action by the parties to this agreement, such provision shall be
+ reformed to the minimum extent necessary to make such provision
+ valid and enforceable.
+ 4. No term or provision of this License shall be deemed waived and no
+ breach consented to unless such waiver or consent shall be in
+ writing and signed by the party to be charged with such waiver or
+ consent.
+ 5. This License constitutes the entire agreement between the parties
+ with respect to the Work licensed here. There are no
+ understandings, agreements or representations with respect to the
+ Work not specified here. Licensor shall not be bound by any
+ additional provisions that may appear in any communication from
+ You. This License may not be modified without the mutual written
+ agreement of the Licensor and You.
+
+Creative Commons is not a party to this License, and makes no warranty
+whatsoever in connection with the Work. Creative Commons will not be
+liable to You or any party on any legal theory for any damages
+whatsoever, including without limitation any general, special,
+incidental or consequential damages arising in connection to this
+license. Notwithstanding the foregoing two (2) sentences, if Creative
+Commons has expressly identified itself as the Licensor hereunder, it
+shall have all rights and obligations of Licensor.
+
+Except for the limited purpose of indicating to the public that the Work
+is licensed under the CCPL, neither party will use the trademark
+"Creative Commons" or any related trademark or logo of Creative Commons
+without the prior written consent of Creative Commons. Any permitted use
+will be in compliance with Creative Commons' then-current trademark
+usage guidelines, as may be published on its website or otherwise made
+available upon request from time to time.
+
+Creative Commons may be contacted at http://creativecommons.org/
+<http://creativecommons.org>.
+
+« Back to Commons Deed <./>
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/tests.py b/docs/tutorials/bfgwiki/src/models/tutorial/tests.py
new file mode 100644
index 000000000..f367b7da0
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/tests.py
@@ -0,0 +1,75 @@
+import unittest
+
+from repoze.bfg import testing
+
+class PageModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Page
+ return Page
+
+ def _makeOne(self, data=u'some data'):
+ return self._getTargetClass()(data=data)
+
+ def test_constructor(self):
+ instance = self._makeOne()
+ self.assertEqual(instance.data, u'some data')
+
+class WikiModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Wiki
+ return Wiki
+
+ def _makeOne(self):
+ return self._getTargetClass()()
+
+ def test_it(self):
+ wiki = self._makeOne()
+ self.assertEqual(wiki.__parent__, None)
+ self.assertEqual(wiki.__name__, None)
+
+class AppmakerTests(unittest.TestCase):
+
+ def _callFUT(self, zodb_root):
+ from tutorial.models import appmaker
+ return appmaker(zodb_root)
+
+ def test_no_app_root(self):
+ root = {}
+ self._callFUT(root)
+ self.assertEqual(root['app_root']['FrontPage'].data,
+ 'This is the front page')
+
+ def test_w_app_root(self):
+ app_root = object()
+ root = {'app_root': app_root}
+ self._callFUT(root)
+ self.failUnless(root['app_root'] is app_root)
+
+class ViewTests(unittest.TestCase):
+
+ """ These tests are unit tests for the view. They test the
+ functionality of *only* the view. They register and use dummy
+ implementations of repoze.bfg functionality to allow you to avoid
+ testing 'too much'"""
+
+ def setUp(self):
+ """ cleanUp() is required to clear out the application registry
+ between tests (done in setUp for good measure too)
+ """
+ testing.cleanUp()
+
+ def tearDown(self):
+ """ cleanUp() is required to clear out the application registry
+ between tests
+ """
+ testing.cleanUp()
+
+ def test_my_view(self):
+ from tutorial.views import my_view
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ renderer = testing.registerDummyRenderer('templates/mytemplate.pt')
+ response = my_view(context, request)
+ renderer.assert_(project='tutorial')
diff --git a/docs/tutorials/bfgwiki/src/models/tutorial/views.py b/docs/tutorials/bfgwiki/src/models/tutorial/views.py
new file mode 100644
index 000000000..13b84352f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/models/tutorial/views.py
@@ -0,0 +1,9 @@
+from repoze.bfg.chameleon_zpt import render_template_to_response
+from repoze.bfg.view import static
+
+static_view = static('templates/static')
+
+def my_view(context, request):
+ return render_template_to_response('templates/mytemplate.pt',
+ request = request,
+ project = 'tutorial')
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/CHANGES.txt b/docs/tutorials/bfgwiki/src/viewdecorators/CHANGES.txt
new file mode 100644
index 000000000..1544cf53b
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/CHANGES.txt
@@ -0,0 +1,3 @@
+0.1
+
+ Initial version
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/README.txt b/docs/tutorials/bfgwiki/src/viewdecorators/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/ez_setup.py b/docs/tutorials/bfgwiki/src/viewdecorators/ez_setup.py
new file mode 100644
index 000000000..d24e845e5
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/ez_setup.py
@@ -0,0 +1,276 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ else:
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+ except pkg_resources.DistributionNotFound:
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/setup.cfg b/docs/tutorials/bfgwiki/src/viewdecorators/setup.cfg
new file mode 100644
index 000000000..8ce7ae0fb
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/setup.cfg
@@ -0,0 +1,2 @@
+[easy_install]
+index_url = http://dist.repoze.org/bfg/current/simple
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/setup.py b/docs/tutorials/bfgwiki/src/viewdecorators/setup.py
new file mode 100644
index 000000000..b289ca8b0
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/setup.py
@@ -0,0 +1,48 @@
+import os
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.txt')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+requires = [
+ 'repoze.bfg',
+ 'docutils',
+ 'ZODB3',
+ 'repoze.zodbconn',
+ 'repoze.tm',
+ ]
+
+setup(name='tutorial',
+ version='0.1',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+ "Topic :: Internet :: WWW/HTTP :: WSGI",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg zope',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=requires,
+ tests_require=requires,
+ test_suite="tutorial",
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial.ini b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial.ini
new file mode 100644
index 000000000..181682585
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+debug = true
+
+[app:zodb]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+[pipeline:main]
+pipeline =
+ egg:repoze.zodbconn#closer
+ egg:Paste#evalerror
+ egg:repoze.tm#tm
+ zodb
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/__init__.py b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/configure.zcml b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/configure.zcml
new file mode 100644
index 000000000..b1501597d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/configure.zcml
@@ -0,0 +1,8 @@
+<configure xmlns="http://namespaces.repoze.org/bfg">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="repoze.bfg.includes" />
+
+ <scan package="."/>
+
+</configure>
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/models.py b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/models.py
new file mode 100644
index 000000000..9761856c6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/models.py
@@ -0,0 +1,22 @@
+from persistent import Persistent
+from persistent.mapping import PersistentMapping
+
+class Wiki(PersistentMapping):
+ __name__ = None
+ __parent__ = None
+
+class Page(Persistent):
+ def __init__(self, data):
+ self.data = data
+
+def appmaker(zodb_root):
+ if not 'app_root' in zodb_root:
+ app_root = Wiki()
+ frontpage = Page('This is the front page')
+ app_root['FrontPage'] = frontpage
+ frontpage.__name__ = 'FrontPage'
+ frontpage.__parent__ = app_root
+ zodb_root['app_root'] = app_root
+ import transaction
+ transaction.commit()
+ return zodb_root['app_root']
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/run.py b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/run.py
new file mode 100644
index 000000000..89953e6de
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/run.py
@@ -0,0 +1,17 @@
+from repoze.bfg.router import make_app
+from repoze.zodbconn.finder import PersistentApplicationFinder
+
+def app(global_config, **kw):
+ """ This function returns a repoze.bfg.router.Router object.
+
+ It is usually called by the PasteDeploy framework during ``paster serve``.
+ """
+ # paster app config callback
+ import tutorial
+ from tutorial.models import appmaker
+ zodb_uri = kw.get('zodb_uri')
+ if zodb_uri is None:
+ raise ValueError("No 'zodb_uri' in application configuration.")
+
+ get_root = PersistentApplicationFinder(zodb_uri, appmaker)
+ return make_app(get_root, tutorial, options=kw)
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/edit.pt b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/edit.pt
new file mode 100644
index 000000000..883ac8b52
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/edit.pt
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
+ <title>bfg tutorial wiki (based on TurboGears 20-Minute Wiki) Editing: ${page.__name__}</title>
+ <link rel="stylesheet" type="text/css"
+ href="${request.application_url}/static/style.css" />
+</head>
+
+<body>
+
+<div class="main_content">
+ <div style="float:right; width: 10em;"> Viewing
+ <span tal:replace="page.__name__">Page Name Goes Here</span> <br/>
+ You can return to the <a href="${request.application_url}">FrontPage</a>.
+ </div>
+
+ <div>
+ <form action="${save_url}" method="post">
+ <textarea name="body" tal:content="page.data" rows="10" cols="60"/>
+ <input type="submit" name="form.submitted" value="Save"/>
+ </form>
+ </div>
+</div>
+</body>
+</html>
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/mytemplate.pt b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..767252554
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/mytemplate.pt
@@ -0,0 +1,99 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<title>${project} Application</title>
+<meta name="keywords" content="python web application" />
+<meta name="description" content="repoze.bfg web application" />
+<link href="${request.application_url}/static/default.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+<!-- start header -->
+<div id="logo">
+ <h2><code>${project}</code>, a <code>repoze.bfg</code> application</h2>
+</div>
+<div id="header">
+ <div id="menu">
+ </div>
+</div>
+<!-- end header -->
+<div id="wrapper">
+ <!-- start page -->
+ <div id="page">
+ <!-- start content -->
+ <div id="content">
+ <div class="post">
+ <h1 class="title">Welcome to <code>${project}</code>, an
+ application generated by the <a
+ href="http://bfg.repoze.org">repoze.bfg</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>repoze.bfg</code> Documentation</h2>
+ <form method="get"
+ action="http://bfg.repoze.org/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>repoze.bfg</code> links</h2>
+ <ul>
+ <li><a
+ href="http://docs.repoze.org/bfg/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#repoze">IRC Channel</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <!-- end sidebar -->
+ <div style="clear: both;">&nbsp;</div>
+ </div>
+</div>
+<!-- end page -->
+<!-- start footer -->
+<div id="footer">
+ <p id="legal">( c ) 2008. All Rights Reserved. Template design
+ by <a href="http://www.freecsstemplates.org/">Free CSS
+ Templates</a>.</p>
+</div>
+<!-- end footer -->
+</body>
+</html>
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/default.css b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/default.css
@@ -0,0 +1,380 @@
+/*
+Design by Free CSS Templates
+http://www.freecsstemplates.org
+Released for free under a Creative Commons Attribution 2.5 License
+*/
+
+body {
+ margin: 0;
+ padding: 0;
+ background: url(images/img01.gif) repeat-x left top;
+ font-size: 13px;
+ font-family: "Trebuchet MS", Georgia, "Times New Roman", Times, serif;
+ text-align: justify;
+ color: #FFFFFF;
+}
+
+h1, h2, h3 {
+ margin: 0;
+ text-transform: lowercase;
+ font-weight: normal;
+ color: #FFFFFF;
+}
+
+h1 {
+ letter-spacing: -1px;
+ font-size: 32px;
+}
+
+h2 {
+ font-size: 23px;
+}
+
+p, ul, ol {
+ margin: 0 0 2em 0;
+ text-align: justify;
+ line-height: 26px;
+}
+
+a:link {
+ color: #8BD80E;
+}
+
+a:hover, a:active {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+a:visited {
+ color: #8BD80E;
+}
+
+img {
+ border: none;
+}
+
+img.left {
+ float: left;
+ margin-right: 15px;
+}
+
+img.right {
+ float: right;
+ margin-left: 15px;
+}
+
+/* Form */
+
+form {
+ margin: 0;
+ padding: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+legend {
+ display: none;
+}
+
+input, textarea, select {
+ font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
+ font-size: 13px;
+ color: #333333;
+}
+
+#wrapper {
+ margin: 0;
+ padding: 0;
+ background: #000000;
+}
+
+/* Header */
+
+#header {
+ width: 713px;
+ margin: 0 auto;
+ height: 42px;
+}
+
+/* Menu */
+
+#menu {
+ float: left;
+ width: 713px;
+ height: 50px;
+ background: url(images/img02.gif) no-repeat left top;
+}
+
+#menu ul {
+ margin: 0;
+ padding: 0px 0 0 10px;
+ list-style: none;
+ line-height: normal;
+}
+
+#menu li {
+ display: block;
+ float: left;
+}
+
+#menu a {
+ display: block;
+ float: left;
+ background: url(images/img04.gif) no-repeat right 55%;
+ margin-top: 5px;
+ margin-right: 3px;
+ padding: 8px 17px;
+ text-decoration: none;
+ font-size: 13px;
+ color: #000000;
+}
+
+#menu a:hover {
+ color: #000000;
+}
+
+#menu .current_page_item a {
+ color: #000000;
+}
+
+/** LOGO */
+
+#logo {
+ width: 713px;
+ height: 80px;
+ margin: 0 auto;
+}
+
+#logo h1, #logo h2 {
+ float: left;
+ margin: 0;
+ padding: 30px 0 0 0px;
+ line-height: normal;
+}
+
+#logo h1 {
+ font-family: Georgia, "Times New Roman", Times, serif;
+ font-size:40px;
+}
+
+#logo h1 a {
+ text-decoration: none;
+ color: #4C4C4C;
+}
+
+#logo h1 a:hover { text-decoration: underline; }
+
+#logo h2 {
+ float: left;
+ padding: 45px 0 0 18px;
+ font: 18px Georgia, "Times New Roman", Times, serif;
+ color: #8BD80E;
+}
+
+#logo p a {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+#logo p a:hover { text-decoration: underline; }
+
+
+
+/* Page */
+
+#page {
+ width: 663px;
+ margin: 0 auto;
+ background: #4C4C4C url(images/img03.gif) no-repeat left bottom;
+ padding: 0 25px;
+}
+
+/* Content */
+
+#content {
+ float: left;
+ width: 410px;
+
+}
+
+/* Post */
+
+.post {
+ padding: 15px 0px;
+ margin-bottom: 20px;
+}
+
+.post .title {
+ margin-bottom: 20px;
+ padding-bottom: 5px;
+}
+
+.post h1 {
+ padding: 0px 0 0 0px;
+ background: url(images/img08.jpg) no-repeat left top;
+ font-size: 24px;
+ color: #FFFFFF;
+}
+
+.post h2 {
+ padding: 0px 0 0 0px;
+ font-size: 22px;
+ color: #FFFFFF;
+}
+
+.post .entry {
+}
+
+.post .meta {
+ padding: 15px 15px 30px 0px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+}
+
+.post .meta p {
+ margin: 0;
+ padding-top: 15px;
+ line-height: normal;
+ color: #FFFFFF;
+}
+
+.post .meta .byline {
+ float: left;
+}
+
+.post .meta .links {
+ float: right;
+}
+
+.post .meta .more {
+ padding: 0 10px 0 18px;
+}
+
+.post .meta .comments {
+}
+
+.post .meta b {
+ display: none;
+}
+
+
+/* Sidebar */
+
+#sidebar {
+ width: 210px;
+ float: right;
+ margin: 0;
+ padding: 0;
+}
+
+#sidebar ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+#sidebar li {
+ margin-bottom: 40px;
+}
+
+#sidebar li ul {
+}
+
+#sidebar li li {
+ margin: 0;
+}
+
+#sidebar h2 {
+ width: 250px;
+ padding: 8px 0 0 0px;
+ margin-bottom: 10px;
+ background: url(images/img07.jpg) no-repeat left top;
+ font-size: 20px;
+ color: #FFFFFF;
+}
+
+/* Search */
+
+#search {
+
+}
+
+#search h2 {
+ margin-bottom: 20px;
+}
+
+#s {
+ width: 140px;
+ margin-right: 5px;
+ padding: 3px;
+ border: 1px solid #BED99C;
+}
+
+#x {
+ padding: 3px;
+ border: none;
+ background: #8BD80E;
+ text-transform: lowercase;
+ font-size: 11px;
+ color: #FFFFFF;
+}
+
+/* Boxes */
+
+.box1 {
+ padding: 20px;
+}
+
+.box2 {
+ color: #BABABA;
+}
+
+.box2 h2 {
+ margin-bottom: 15px;
+ font-size: 16px;
+ color: #FFFFFF;
+}
+
+.box2 ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.box2 a:link, .box2 a:hover, .box2 a:active, .box2 a:visited {
+ color: #EDEDED;
+}
+
+/* Footer */
+#footer-wrap {
+}
+
+#footer {
+ margin: 0 auto;
+ padding: 20px 0 10px 0;
+ background: #000000;
+}
+
+html>body #footer {
+ height: auto;
+}
+
+#footer p {
+ font-size: 11px;
+}
+
+#legal {
+ clear: both;
+ padding-top: 17px;
+ text-align: center;
+ color: #FFFFFF;
+}
+
+#legal a {
+ font-weight: normal;
+ color: #FFFFFF;
+}
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img01.gif b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img02.gif b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img03.gif b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img04.gif b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/spacer.gif b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/style.css b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/style.css
new file mode 100644
index 000000000..0a4b5767e
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/style.css
@@ -0,0 +1,109 @@
+html, body {
+ color: black;
+ background-color: #ddd;
+ font: x-small "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif;
+ margin: 0;
+ padding: 0;
+}
+
+td, th {padding:3px;border:none;}
+tr th {text-align:left;background-color:#f0f0f0;color:#333;}
+tr.odd td {background-color:#edf3fe;}
+tr.even td {background-color:#fff;}
+
+#header {
+ height: 80px;
+ width: 777px;
+ background: blue URL('../images/header_inner.png') no-repeat;
+ border-left: 1px solid #aaa;
+ border-right: 1px solid #aaa;
+ margin: 0 auto 0 auto;
+}
+
+a.link, a, a.active {
+ color: #369;
+}
+
+
+#main_content {
+ color: black;
+ font-size: 127%;
+ background-color: white;
+ width: 757px;
+ margin: 0 auto 0 auto;
+ border-left: 1px solid #aaa;
+ border-right: 1px solid #aaa;
+ padding: 10px;
+}
+
+#sidebar {
+ border: 1px solid #aaa;
+ background-color: #eee;
+ margin: 0.5em;
+ padding: 1em;
+ float: right;
+ width: 200px;
+ font-size: 88%;
+}
+
+#sidebar h2 {
+ margin-top: 0;
+}
+
+#sidebar ul {
+ margin-left: 1.5em;
+ padding-left: 0;
+}
+
+h1,h2,h3,h4,h5,h6,#getting_started_steps {
+ font-family: "Century Schoolbook L", Georgia, serif;
+ font-weight: bold;
+}
+
+h2 {
+ font-size: 150%;
+}
+
+#footer {
+ border: 1px solid #aaa;
+ border-top: 0px none;
+ color: #999;
+ background-color: white;
+ padding: 10px;
+ font-size: 80%;
+ text-align: center;
+ width: 757px;
+ margin: 0 auto 1em auto;
+}
+
+.code {
+ font-family: monospace;
+}
+
+span.code {
+ font-weight: bold;
+ background: #eee;
+}
+
+#status_block {
+ margin: 0 auto 0.5em auto;
+ padding: 15px 10px 15px 55px;
+ background: #cec URL('../images/ok.png') left center no-repeat;
+ border: 1px solid #9c9;
+ width: 450px;
+ font-size: 120%;
+ font-weight: bolder;
+}
+
+.notice {
+ margin: 0.5em auto 0.5em auto;
+ padding: 15px 10px 15px 55px;
+ width: 450px;
+ background: #eef URL('../images/info.png') left center no-repeat;
+ border: 1px solid #cce;
+}
+
+.fielderror {
+ color: red;
+ font-weight: bold;
+}
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/templatelicense.txt b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/static/templatelicense.txt
@@ -0,0 +1,243 @@
+Creative Commons </>
+
+Creative Commons Legal Code
+
+*Attribution 2.5*
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
+ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE
+INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ITS USE.
+
+/License/
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE
+RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS
+AND CONDITIONS.
+
+*1. Definitions*
+
+ 1. *"Collective Work"* means a work, such as a periodical issue,
+ anthology or encyclopedia, in which the Work in its entirety in
+ unmodified form, along with a number of other contributions,
+ constituting separate and independent works in themselves, are
+ assembled into a collective whole. A work that constitutes a
+ Collective Work will not be considered a Derivative Work (as
+ defined below) for the purposes of this License.
+ 2. *"Derivative Work"* means a work based upon the Work or upon the
+ Work and other pre-existing works, such as a translation, musical
+ arrangement, dramatization, fictionalization, motion picture
+ version, sound recording, art reproduction, abridgment,
+ condensation, or any other form in which the Work may be recast,
+ transformed, or adapted, except that a work that constitutes a
+ Collective Work will not be considered a Derivative Work for the
+ purpose of this License. For the avoidance of doubt, where the
+ Work is a musical composition or sound recording, the
+ synchronization of the Work in timed-relation with a moving image
+ ("synching") will be considered a Derivative Work for the purpose
+ of this License.
+ 3. *"Licensor"* means the individual or entity that offers the Work
+ under the terms of this License.
+ 4. *"Original Author"* means the individual or entity who created the
+ Work.
+ 5. *"Work"* means the copyrightable work of authorship offered under
+ the terms of this License.
+ 6. *"You"* means an individual or entity exercising rights under this
+ License who has not previously violated the terms of this License
+ with respect to the Work, or who has received express permission
+ from the Licensor to exercise rights under this License despite a
+ previous violation.
+
+*2. Fair Use Rights.* Nothing in this license is intended to reduce,
+limit, or restrict any rights arising from fair use, first sale or other
+limitations on the exclusive rights of the copyright owner under
+copyright law or other applicable laws.
+
+*3. License Grant.* Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ 1. to reproduce the Work, to incorporate the Work into one or more
+ Collective Works, and to reproduce the Work as incorporated in the
+ Collective Works;
+ 2. to create and reproduce Derivative Works;
+ 3. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission the Work including as incorporated in Collective Works;
+ 4. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission Derivative Works.
+ 5.
+
+ For the avoidance of doubt, where the work is a musical composition:
+
+ 1. *Performance Royalties Under Blanket Licenses*. Licensor
+ waives the exclusive right to collect, whether individually
+ or via a performance rights society (e.g. ASCAP, BMI,
+ SESAC), royalties for the public performance or public
+ digital performance (e.g. webcast) of the Work.
+ 2. *Mechanical Rights and Statutory Royalties*. Licensor waives
+ the exclusive right to collect, whether individually or via
+ a music rights agency or designated agent (e.g. Harry Fox
+ Agency), royalties for any phonorecord You create from the
+ Work ("cover version") and distribute, subject to the
+ compulsory license created by 17 USC Section 115 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+ 6. *Webcasting Rights and Statutory Royalties*. For the avoidance of
+ doubt, where the Work is a sound recording, Licensor waives the
+ exclusive right to collect, whether individually or via a
+ performance-rights society (e.g. SoundExchange), royalties for the
+ public digital performance (e.g. webcast) of the Work, subject to
+ the compulsory license created by 17 USC Section 114 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights
+in other media and formats. All rights not expressly granted by Licensor
+are hereby reserved.
+
+*4. Restrictions.*The license granted in Section 3 above is expressly
+made subject to and limited by the following restrictions:
+
+ 1. You may distribute, publicly display, publicly perform, or
+ publicly digitally perform the Work only under the terms of this
+ License, and You must include a copy of, or the Uniform Resource
+ Identifier for, this License with every copy or phonorecord of the
+ Work You distribute, publicly display, publicly perform, or
+ publicly digitally perform. You may not offer or impose any terms
+ on the Work that alter or restrict the terms of this License or
+ the recipients' exercise of the rights granted hereunder. You may
+ not sublicense the Work. You must keep intact all notices that
+ refer to this License and to the disclaimer of warranties. You may
+ not distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work with any technological measures that
+ control access or use of the Work in a manner inconsistent with
+ the terms of this License Agreement. The above applies to the Work
+ as incorporated in a Collective Work, but this does not require
+ the Collective Work apart from the Work itself to be made subject
+ to the terms of this License. If You create a Collective Work,
+ upon notice from any Licensor You must, to the extent practicable,
+ remove from the Collective Work any credit as required by clause
+ 4(b), as requested. If You create a Derivative Work, upon notice
+ from any Licensor You must, to the extent practicable, remove from
+ the Derivative Work any credit as required by clause 4(b), as
+ requested.
+ 2. If you distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work or any Derivative Works or Collective
+ Works, You must keep intact all copyright notices for the Work and
+ provide, reasonable to the medium or means You are utilizing: (i)
+ the name of the Original Author (or pseudonym, if applicable) if
+ supplied, and/or (ii) if the Original Author and/or Licensor
+ designate another party or parties (e.g. a sponsor institute,
+ publishing entity, journal) for attribution in Licensor's
+ copyright notice, terms of service or by other reasonable means,
+ the name of such party or parties; the title of the Work if
+ supplied; to the extent reasonably practicable, the Uniform
+ Resource Identifier, if any, that Licensor specifies to be
+ associated with the Work, unless such URI does not refer to the
+ copyright notice or licensing information for the Work; and in the
+ case of a Derivative Work, a credit identifying the use of the
+ Work in the Derivative Work (e.g., "French translation of the Work
+ by Original Author," or "Screenplay based on original Work by
+ Original Author"). Such credit may be implemented in any
+ reasonable manner; provided, however, that in the case of a
+ Derivative Work or Collective Work, at a minimum such credit will
+ appear where any other comparable authorship credit appears and in
+ a manner at least as prominent as such other comparable authorship
+ credit.
+
+*5. Representations, Warranties and Disclaimer*
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE
+EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+*6. Limitation on Liability.* EXCEPT TO THE EXTENT REQUIRED BY
+APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL
+THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY
+DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF
+LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+*7. Termination*
+
+ 1. This License and the rights granted hereunder will terminate
+ automatically upon any breach by You of the terms of this License.
+ Individuals or entities who have received Derivative Works or
+ Collective Works from You under this License, however, will not
+ have their licenses terminated provided such individuals or
+ entities remain in full compliance with those licenses. Sections
+ 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
+ 2. Subject to the above terms and conditions, the license granted
+ here is perpetual (for the duration of the applicable copyright in
+ the Work). Notwithstanding the above, Licensor reserves the right
+ to release the Work under different license terms or to stop
+ distributing the Work at any time; provided, however that any such
+ election will not serve to withdraw this License (or any other
+ license that has been, or is required to be, granted under the
+ terms of this License), and this License will continue in full
+ force and effect unless terminated as stated above.
+
+*8. Miscellaneous*
+
+ 1. Each time You distribute or publicly digitally perform the Work or
+ a Collective Work, the Licensor offers to the recipient a license
+ to the Work on the same terms and conditions as the license
+ granted to You under this License.
+ 2. Each time You distribute or publicly digitally perform a
+ Derivative Work, Licensor offers to the recipient a license to the
+ original Work on the same terms and conditions as the license
+ granted to You under this License.
+ 3. If any provision of this License is invalid or unenforceable under
+ applicable law, it shall not affect the validity or enforceability
+ of the remainder of the terms of this License, and without further
+ action by the parties to this agreement, such provision shall be
+ reformed to the minimum extent necessary to make such provision
+ valid and enforceable.
+ 4. No term or provision of this License shall be deemed waived and no
+ breach consented to unless such waiver or consent shall be in
+ writing and signed by the party to be charged with such waiver or
+ consent.
+ 5. This License constitutes the entire agreement between the parties
+ with respect to the Work licensed here. There are no
+ understandings, agreements or representations with respect to the
+ Work not specified here. Licensor shall not be bound by any
+ additional provisions that may appear in any communication from
+ You. This License may not be modified without the mutual written
+ agreement of the Licensor and You.
+
+Creative Commons is not a party to this License, and makes no warranty
+whatsoever in connection with the Work. Creative Commons will not be
+liable to You or any party on any legal theory for any damages
+whatsoever, including without limitation any general, special,
+incidental or consequential damages arising in connection to this
+license. Notwithstanding the foregoing two (2) sentences, if Creative
+Commons has expressly identified itself as the Licensor hereunder, it
+shall have all rights and obligations of Licensor.
+
+Except for the limited purpose of indicating to the public that the Work
+is licensed under the CCPL, neither party will use the trademark
+"Creative Commons" or any related trademark or logo of Creative Commons
+without the prior written consent of Creative Commons. Any permitted use
+will be in compliance with Creative Commons' then-current trademark
+usage guidelines, as may be published on its website or otherwise made
+available upon request from time to time.
+
+Creative Commons may be contacted at http://creativecommons.org/
+<http://creativecommons.org>.
+
+« Back to Commons Deed <./>
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/view.pt b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/view.pt
new file mode 100644
index 000000000..5326e6454
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/templates/view.pt
@@ -0,0 +1,26 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
+ <title>${page.__name__} - bfg tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+ <link rel="stylesheet" type="text/css"
+ href="${request.application_url}/static/style.css" />
+</head>
+
+<body>
+
+<div class="main_content">
+<div style="float:right; width: 10em;"> Viewing
+<span tal:replace="page.__name__">Page Name Goes Here</span> <br/>
+You can return to the <a href="${request.application_url}">FrontPage</a>.
+</div>
+
+<div tal:replace="structure content">Page text goes here.</div>
+<p><a tal:attributes="href edit_url" href="">Edit this page</a></p>
+</div>
+
+</body></html>
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/tests.py b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/tests.py
new file mode 100644
index 000000000..84a3a0f37
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/tests.py
@@ -0,0 +1,150 @@
+import unittest
+
+from repoze.bfg import testing
+
+class PageModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Page
+ return Page
+
+ def _makeOne(self, data=u'some data'):
+ return self._getTargetClass()(data=data)
+
+ def test_constructor(self):
+ instance = self._makeOne()
+ self.assertEqual(instance.data, u'some data')
+
+class WikiModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Wiki
+ return Wiki
+
+ def _makeOne(self):
+ return self._getTargetClass()()
+
+ def test_it(self):
+ wiki = self._makeOne()
+ self.assertEqual(wiki.__parent__, None)
+ self.assertEqual(wiki.__name__, None)
+
+class AppmakerTests(unittest.TestCase):
+ def _callFUT(self, zodb_root):
+ from tutorial.models import appmaker
+ return appmaker(zodb_root)
+
+ def test_it(self):
+ root = {}
+ self._callFUT(root)
+ self.assertEqual(root['app_root']['FrontPage'].data,
+ 'This is the front page')
+
+class ViewWikiTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def test_it(self):
+ from tutorial.views import view_wiki
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ response = view_wiki(context, request)
+ self.assertEqual(response.location, 'http://example.com/FrontPage')
+
+class ViewPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import view_page
+ return view_page(context, request)
+
+ def test_it(self):
+ wiki = testing.DummyModel()
+ wiki['IDoExist'] = testing.DummyModel()
+ context = testing.DummyModel(data='Hello CruelWorld IDoExist')
+ context.__parent__ = wiki
+ context.__name__ = 'thepage'
+ request = testing.DummyRequest()
+ renderer = testing.registerDummyRenderer('templates/view.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(
+ renderer.content,
+ '<div class="document">\n'
+ '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
+ 'CruelWorld</a> '
+ '<a href="http://example.com/IDoExist/">'
+ 'IDoExist</a>'
+ '</p>\n</div>\n')
+ self.assertEqual(renderer.edit_url,
+ 'http://example.com/thepage/edit_page')
+
+
+class AddPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import add_page
+ return add_page(context, request)
+
+ def test_it_notsubmitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ request.subpath = ['AnotherPage']
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(renderer.page.data, '')
+
+ def test_it_submitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ request.subpath = ['AnotherPage']
+ response = self._callFUT(context, request)
+ page = context['AnotherPage']
+ self.assertEqual(page.data, 'Hello yo!')
+ self.assertEqual(page.__name__, 'AnotherPage')
+ self.assertEqual(page.__parent__, context)
+
+class EditPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import edit_page
+ return edit_page(context, request)
+
+ def test_it_notsubmitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(renderer.page, context)
+
+ def test_it_submitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(response.location, 'http://example.com/')
+ self.assertEqual(context.data, 'Hello yo!')
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/views.py b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/views.py
new file mode 100644
index 000000000..aae1c30ce
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/viewdecorators/tutorial/views.py
@@ -0,0 +1,81 @@
+from docutils.core import publish_parts
+import re
+
+from webob.exc import HTTPFound
+from repoze.bfg.url import model_url
+from repoze.bfg.chameleon_zpt import render_template_to_response
+from repoze.bfg.view import static
+from repoze.bfg.view import bfg_view
+
+from tutorial.models import Page
+from tutorial.models import Wiki
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+static_app = static('templates/static')
+
+@bfg_view(for_=Wiki, name='static')
+def static_view(context, request):
+ return static_app(context, request)
+
+@bfg_view(for_=Wiki)
+def view_wiki(context, request):
+ return HTTPFound(location = model_url(context, request, 'FrontPage'))
+
+@bfg_view(for_=Page)
+def view_page(context, request):
+ wiki = context.__parent__
+
+ def check(match):
+ word = match.group(1)
+ if word in wiki:
+ page = wiki[word]
+ view_url = model_url(page, request)
+ return '<a href="%s">%s</a>' % (view_url, word)
+ else:
+ add_url = request.application_url + '/add_page/' + word
+ return '<a href="%s">%s</a>' % (add_url, word)
+
+ content = publish_parts(context.data, writer_name='html')['html_body']
+ content = wikiwords.sub(check, content)
+ edit_url = model_url(context, request, 'edit_page')
+ return render_template_to_response('templates/view.pt',
+ request = request,
+ page = context,
+ content = content,
+ edit_url = edit_url)
+
+@bfg_view(for_=Wiki, name='add_page')
+def add_page(context, request):
+ name = request.subpath[0]
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(body)
+ page.__name__ = name
+ page.__parent__ = context
+ context[name] = page
+ return HTTPFound(location = model_url(page, request))
+ save_url = model_url(context, request, 'add_page', name)
+ page = Page('')
+ page.__name__ = name
+ page.__parent__ = context
+ return render_template_to_response('templates/edit.pt',
+ request = request,
+ page = page,
+ save_url = save_url)
+
+@bfg_view(for_=Page, name='edit_page')
+def edit_page(context, request):
+ if 'form.submitted' in request.params:
+ context.data = request.params['body']
+ return HTTPFound(location = model_url(context, request))
+
+ return render_template_to_response('templates/edit.pt',
+ request = request,
+ page = context,
+ save_url = model_url(context, request,
+ 'edit_page')
+ )
+
+
diff --git a/docs/tutorials/bfgwiki/src/views/CHANGES.txt b/docs/tutorials/bfgwiki/src/views/CHANGES.txt
new file mode 100644
index 000000000..1544cf53b
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/CHANGES.txt
@@ -0,0 +1,3 @@
+0.1
+
+ Initial version
diff --git a/docs/tutorials/bfgwiki/src/views/README.txt b/docs/tutorials/bfgwiki/src/views/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/views/ez_setup.py b/docs/tutorials/bfgwiki/src/views/ez_setup.py
new file mode 100644
index 000000000..d24e845e5
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/ez_setup.py
@@ -0,0 +1,276 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ else:
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+ except pkg_resources.DistributionNotFound:
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/views/setup.cfg b/docs/tutorials/bfgwiki/src/views/setup.cfg
new file mode 100644
index 000000000..8ce7ae0fb
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/setup.cfg
@@ -0,0 +1,2 @@
+[easy_install]
+index_url = http://dist.repoze.org/bfg/current/simple
diff --git a/docs/tutorials/bfgwiki/src/views/setup.py b/docs/tutorials/bfgwiki/src/views/setup.py
new file mode 100644
index 000000000..b289ca8b0
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/setup.py
@@ -0,0 +1,48 @@
+import os
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.txt')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+requires = [
+ 'repoze.bfg',
+ 'docutils',
+ 'ZODB3',
+ 'repoze.zodbconn',
+ 'repoze.tm',
+ ]
+
+setup(name='tutorial',
+ version='0.1',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+ "Topic :: Internet :: WWW/HTTP :: WSGI",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg zope',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=requires,
+ tests_require=requires,
+ test_suite="tutorial",
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial.ini b/docs/tutorials/bfgwiki/src/views/tutorial.ini
new file mode 100644
index 000000000..181682585
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+debug = true
+
+[app:zodb]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+[pipeline:main]
+pipeline =
+ egg:repoze.zodbconn#closer
+ egg:Paste#evalerror
+ egg:repoze.tm#tm
+ zodb
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/__init__.py b/docs/tutorials/bfgwiki/src/views/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/configure.zcml b/docs/tutorials/bfgwiki/src/views/tutorial/configure.zcml
new file mode 100644
index 000000000..fda2ce0e8
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/configure.zcml
@@ -0,0 +1,34 @@
+<configure xmlns="http://namespaces.repoze.org/bfg">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="repoze.bfg.includes" />
+
+ <view
+ for=".models.Wiki"
+ view=".views.static_view"
+ name="static"
+ />
+
+ <view
+ for=".models.Wiki"
+ view=".views.view_wiki"
+ />
+
+ <view
+ for=".models.Wiki"
+ name="add_page"
+ view=".views.add_page"
+ />
+
+ <view
+ for=".models.Page"
+ view=".views.view_page"
+ />
+
+ <view
+ for=".models.Page"
+ name="edit_page"
+ view=".views.edit_page"
+ />
+
+</configure>
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/models.py b/docs/tutorials/bfgwiki/src/views/tutorial/models.py
new file mode 100644
index 000000000..9761856c6
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/models.py
@@ -0,0 +1,22 @@
+from persistent import Persistent
+from persistent.mapping import PersistentMapping
+
+class Wiki(PersistentMapping):
+ __name__ = None
+ __parent__ = None
+
+class Page(Persistent):
+ def __init__(self, data):
+ self.data = data
+
+def appmaker(zodb_root):
+ if not 'app_root' in zodb_root:
+ app_root = Wiki()
+ frontpage = Page('This is the front page')
+ app_root['FrontPage'] = frontpage
+ frontpage.__name__ = 'FrontPage'
+ frontpage.__parent__ = app_root
+ zodb_root['app_root'] = app_root
+ import transaction
+ transaction.commit()
+ return zodb_root['app_root']
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/run.py b/docs/tutorials/bfgwiki/src/views/tutorial/run.py
new file mode 100644
index 000000000..89953e6de
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/run.py
@@ -0,0 +1,17 @@
+from repoze.bfg.router import make_app
+from repoze.zodbconn.finder import PersistentApplicationFinder
+
+def app(global_config, **kw):
+ """ This function returns a repoze.bfg.router.Router object.
+
+ It is usually called by the PasteDeploy framework during ``paster serve``.
+ """
+ # paster app config callback
+ import tutorial
+ from tutorial.models import appmaker
+ zodb_uri = kw.get('zodb_uri')
+ if zodb_uri is None:
+ raise ValueError("No 'zodb_uri' in application configuration.")
+
+ get_root = PersistentApplicationFinder(zodb_uri, appmaker)
+ return make_app(get_root, tutorial, options=kw)
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/edit.pt b/docs/tutorials/bfgwiki/src/views/tutorial/templates/edit.pt
new file mode 100644
index 000000000..883ac8b52
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/edit.pt
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
+ <title>bfg tutorial wiki (based on TurboGears 20-Minute Wiki) Editing: ${page.__name__}</title>
+ <link rel="stylesheet" type="text/css"
+ href="${request.application_url}/static/style.css" />
+</head>
+
+<body>
+
+<div class="main_content">
+ <div style="float:right; width: 10em;"> Viewing
+ <span tal:replace="page.__name__">Page Name Goes Here</span> <br/>
+ You can return to the <a href="${request.application_url}">FrontPage</a>.
+ </div>
+
+ <div>
+ <form action="${save_url}" method="post">
+ <textarea name="body" tal:content="page.data" rows="10" cols="60"/>
+ <input type="submit" name="form.submitted" value="Save"/>
+ </form>
+ </div>
+</div>
+</body>
+</html>
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/bfgwiki/src/views/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..767252554
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/mytemplate.pt
@@ -0,0 +1,99 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<title>${project} Application</title>
+<meta name="keywords" content="python web application" />
+<meta name="description" content="repoze.bfg web application" />
+<link href="${request.application_url}/static/default.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+<!-- start header -->
+<div id="logo">
+ <h2><code>${project}</code>, a <code>repoze.bfg</code> application</h2>
+</div>
+<div id="header">
+ <div id="menu">
+ </div>
+</div>
+<!-- end header -->
+<div id="wrapper">
+ <!-- start page -->
+ <div id="page">
+ <!-- start content -->
+ <div id="content">
+ <div class="post">
+ <h1 class="title">Welcome to <code>${project}</code>, an
+ application generated by the <a
+ href="http://bfg.repoze.org">repoze.bfg</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>repoze.bfg</code> Documentation</h2>
+ <form method="get"
+ action="http://bfg.repoze.org/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>repoze.bfg</code> links</h2>
+ <ul>
+ <li><a
+ href="http://docs.repoze.org/bfg/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://docs.repoze.org/bfg/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#repoze">IRC Channel</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <!-- end sidebar -->
+ <div style="clear: both;">&nbsp;</div>
+ </div>
+</div>
+<!-- end page -->
+<!-- start footer -->
+<div id="footer">
+ <p id="legal">( c ) 2008. All Rights Reserved. Template design
+ by <a href="http://www.freecsstemplates.org/">Free CSS
+ Templates</a>.</p>
+</div>
+<!-- end footer -->
+</body>
+</html>
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/default.css b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/default.css
@@ -0,0 +1,380 @@
+/*
+Design by Free CSS Templates
+http://www.freecsstemplates.org
+Released for free under a Creative Commons Attribution 2.5 License
+*/
+
+body {
+ margin: 0;
+ padding: 0;
+ background: url(images/img01.gif) repeat-x left top;
+ font-size: 13px;
+ font-family: "Trebuchet MS", Georgia, "Times New Roman", Times, serif;
+ text-align: justify;
+ color: #FFFFFF;
+}
+
+h1, h2, h3 {
+ margin: 0;
+ text-transform: lowercase;
+ font-weight: normal;
+ color: #FFFFFF;
+}
+
+h1 {
+ letter-spacing: -1px;
+ font-size: 32px;
+}
+
+h2 {
+ font-size: 23px;
+}
+
+p, ul, ol {
+ margin: 0 0 2em 0;
+ text-align: justify;
+ line-height: 26px;
+}
+
+a:link {
+ color: #8BD80E;
+}
+
+a:hover, a:active {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+a:visited {
+ color: #8BD80E;
+}
+
+img {
+ border: none;
+}
+
+img.left {
+ float: left;
+ margin-right: 15px;
+}
+
+img.right {
+ float: right;
+ margin-left: 15px;
+}
+
+/* Form */
+
+form {
+ margin: 0;
+ padding: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+legend {
+ display: none;
+}
+
+input, textarea, select {
+ font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
+ font-size: 13px;
+ color: #333333;
+}
+
+#wrapper {
+ margin: 0;
+ padding: 0;
+ background: #000000;
+}
+
+/* Header */
+
+#header {
+ width: 713px;
+ margin: 0 auto;
+ height: 42px;
+}
+
+/* Menu */
+
+#menu {
+ float: left;
+ width: 713px;
+ height: 50px;
+ background: url(images/img02.gif) no-repeat left top;
+}
+
+#menu ul {
+ margin: 0;
+ padding: 0px 0 0 10px;
+ list-style: none;
+ line-height: normal;
+}
+
+#menu li {
+ display: block;
+ float: left;
+}
+
+#menu a {
+ display: block;
+ float: left;
+ background: url(images/img04.gif) no-repeat right 55%;
+ margin-top: 5px;
+ margin-right: 3px;
+ padding: 8px 17px;
+ text-decoration: none;
+ font-size: 13px;
+ color: #000000;
+}
+
+#menu a:hover {
+ color: #000000;
+}
+
+#menu .current_page_item a {
+ color: #000000;
+}
+
+/** LOGO */
+
+#logo {
+ width: 713px;
+ height: 80px;
+ margin: 0 auto;
+}
+
+#logo h1, #logo h2 {
+ float: left;
+ margin: 0;
+ padding: 30px 0 0 0px;
+ line-height: normal;
+}
+
+#logo h1 {
+ font-family: Georgia, "Times New Roman", Times, serif;
+ font-size:40px;
+}
+
+#logo h1 a {
+ text-decoration: none;
+ color: #4C4C4C;
+}
+
+#logo h1 a:hover { text-decoration: underline; }
+
+#logo h2 {
+ float: left;
+ padding: 45px 0 0 18px;
+ font: 18px Georgia, "Times New Roman", Times, serif;
+ color: #8BD80E;
+}
+
+#logo p a {
+ text-decoration: none;
+ color: #8BD80E;
+}
+
+#logo p a:hover { text-decoration: underline; }
+
+
+
+/* Page */
+
+#page {
+ width: 663px;
+ margin: 0 auto;
+ background: #4C4C4C url(images/img03.gif) no-repeat left bottom;
+ padding: 0 25px;
+}
+
+/* Content */
+
+#content {
+ float: left;
+ width: 410px;
+
+}
+
+/* Post */
+
+.post {
+ padding: 15px 0px;
+ margin-bottom: 20px;
+}
+
+.post .title {
+ margin-bottom: 20px;
+ padding-bottom: 5px;
+}
+
+.post h1 {
+ padding: 0px 0 0 0px;
+ background: url(images/img08.jpg) no-repeat left top;
+ font-size: 24px;
+ color: #FFFFFF;
+}
+
+.post h2 {
+ padding: 0px 0 0 0px;
+ font-size: 22px;
+ color: #FFFFFF;
+}
+
+.post .entry {
+}
+
+.post .meta {
+ padding: 15px 15px 30px 0px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+}
+
+.post .meta p {
+ margin: 0;
+ padding-top: 15px;
+ line-height: normal;
+ color: #FFFFFF;
+}
+
+.post .meta .byline {
+ float: left;
+}
+
+.post .meta .links {
+ float: right;
+}
+
+.post .meta .more {
+ padding: 0 10px 0 18px;
+}
+
+.post .meta .comments {
+}
+
+.post .meta b {
+ display: none;
+}
+
+
+/* Sidebar */
+
+#sidebar {
+ width: 210px;
+ float: right;
+ margin: 0;
+ padding: 0;
+}
+
+#sidebar ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+#sidebar li {
+ margin-bottom: 40px;
+}
+
+#sidebar li ul {
+}
+
+#sidebar li li {
+ margin: 0;
+}
+
+#sidebar h2 {
+ width: 250px;
+ padding: 8px 0 0 0px;
+ margin-bottom: 10px;
+ background: url(images/img07.jpg) no-repeat left top;
+ font-size: 20px;
+ color: #FFFFFF;
+}
+
+/* Search */
+
+#search {
+
+}
+
+#search h2 {
+ margin-bottom: 20px;
+}
+
+#s {
+ width: 140px;
+ margin-right: 5px;
+ padding: 3px;
+ border: 1px solid #BED99C;
+}
+
+#x {
+ padding: 3px;
+ border: none;
+ background: #8BD80E;
+ text-transform: lowercase;
+ font-size: 11px;
+ color: #FFFFFF;
+}
+
+/* Boxes */
+
+.box1 {
+ padding: 20px;
+}
+
+.box2 {
+ color: #BABABA;
+}
+
+.box2 h2 {
+ margin-bottom: 15px;
+ font-size: 16px;
+ color: #FFFFFF;
+}
+
+.box2 ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.box2 a:link, .box2 a:hover, .box2 a:active, .box2 a:visited {
+ color: #EDEDED;
+}
+
+/* Footer */
+#footer-wrap {
+}
+
+#footer {
+ margin: 0 auto;
+ padding: 20px 0 10px 0;
+ background: #000000;
+}
+
+html>body #footer {
+ height: auto;
+}
+
+#footer p {
+ font-size: 11px;
+}
+
+#legal {
+ clear: both;
+ padding-top: 17px;
+ text-align: center;
+ color: #FFFFFF;
+}
+
+#legal a {
+ font-weight: normal;
+ color: #FFFFFF;
+}
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img01.gif b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img02.gif b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img03.gif b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img04.gif b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/spacer.gif b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/style.css b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/style.css
new file mode 100644
index 000000000..0a4b5767e
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/style.css
@@ -0,0 +1,109 @@
+html, body {
+ color: black;
+ background-color: #ddd;
+ font: x-small "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif;
+ margin: 0;
+ padding: 0;
+}
+
+td, th {padding:3px;border:none;}
+tr th {text-align:left;background-color:#f0f0f0;color:#333;}
+tr.odd td {background-color:#edf3fe;}
+tr.even td {background-color:#fff;}
+
+#header {
+ height: 80px;
+ width: 777px;
+ background: blue URL('../images/header_inner.png') no-repeat;
+ border-left: 1px solid #aaa;
+ border-right: 1px solid #aaa;
+ margin: 0 auto 0 auto;
+}
+
+a.link, a, a.active {
+ color: #369;
+}
+
+
+#main_content {
+ color: black;
+ font-size: 127%;
+ background-color: white;
+ width: 757px;
+ margin: 0 auto 0 auto;
+ border-left: 1px solid #aaa;
+ border-right: 1px solid #aaa;
+ padding: 10px;
+}
+
+#sidebar {
+ border: 1px solid #aaa;
+ background-color: #eee;
+ margin: 0.5em;
+ padding: 1em;
+ float: right;
+ width: 200px;
+ font-size: 88%;
+}
+
+#sidebar h2 {
+ margin-top: 0;
+}
+
+#sidebar ul {
+ margin-left: 1.5em;
+ padding-left: 0;
+}
+
+h1,h2,h3,h4,h5,h6,#getting_started_steps {
+ font-family: "Century Schoolbook L", Georgia, serif;
+ font-weight: bold;
+}
+
+h2 {
+ font-size: 150%;
+}
+
+#footer {
+ border: 1px solid #aaa;
+ border-top: 0px none;
+ color: #999;
+ background-color: white;
+ padding: 10px;
+ font-size: 80%;
+ text-align: center;
+ width: 757px;
+ margin: 0 auto 1em auto;
+}
+
+.code {
+ font-family: monospace;
+}
+
+span.code {
+ font-weight: bold;
+ background: #eee;
+}
+
+#status_block {
+ margin: 0 auto 0.5em auto;
+ padding: 15px 10px 15px 55px;
+ background: #cec URL('../images/ok.png') left center no-repeat;
+ border: 1px solid #9c9;
+ width: 450px;
+ font-size: 120%;
+ font-weight: bolder;
+}
+
+.notice {
+ margin: 0.5em auto 0.5em auto;
+ padding: 15px 10px 15px 55px;
+ width: 450px;
+ background: #eef URL('../images/info.png') left center no-repeat;
+ border: 1px solid #cce;
+}
+
+.fielderror {
+ color: red;
+ font-weight: bold;
+}
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/templatelicense.txt b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/static/templatelicense.txt
@@ -0,0 +1,243 @@
+Creative Commons </>
+
+Creative Commons Legal Code
+
+*Attribution 2.5*
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
+ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE
+INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ITS USE.
+
+/License/
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE
+RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS
+AND CONDITIONS.
+
+*1. Definitions*
+
+ 1. *"Collective Work"* means a work, such as a periodical issue,
+ anthology or encyclopedia, in which the Work in its entirety in
+ unmodified form, along with a number of other contributions,
+ constituting separate and independent works in themselves, are
+ assembled into a collective whole. A work that constitutes a
+ Collective Work will not be considered a Derivative Work (as
+ defined below) for the purposes of this License.
+ 2. *"Derivative Work"* means a work based upon the Work or upon the
+ Work and other pre-existing works, such as a translation, musical
+ arrangement, dramatization, fictionalization, motion picture
+ version, sound recording, art reproduction, abridgment,
+ condensation, or any other form in which the Work may be recast,
+ transformed, or adapted, except that a work that constitutes a
+ Collective Work will not be considered a Derivative Work for the
+ purpose of this License. For the avoidance of doubt, where the
+ Work is a musical composition or sound recording, the
+ synchronization of the Work in timed-relation with a moving image
+ ("synching") will be considered a Derivative Work for the purpose
+ of this License.
+ 3. *"Licensor"* means the individual or entity that offers the Work
+ under the terms of this License.
+ 4. *"Original Author"* means the individual or entity who created the
+ Work.
+ 5. *"Work"* means the copyrightable work of authorship offered under
+ the terms of this License.
+ 6. *"You"* means an individual or entity exercising rights under this
+ License who has not previously violated the terms of this License
+ with respect to the Work, or who has received express permission
+ from the Licensor to exercise rights under this License despite a
+ previous violation.
+
+*2. Fair Use Rights.* Nothing in this license is intended to reduce,
+limit, or restrict any rights arising from fair use, first sale or other
+limitations on the exclusive rights of the copyright owner under
+copyright law or other applicable laws.
+
+*3. License Grant.* Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ 1. to reproduce the Work, to incorporate the Work into one or more
+ Collective Works, and to reproduce the Work as incorporated in the
+ Collective Works;
+ 2. to create and reproduce Derivative Works;
+ 3. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission the Work including as incorporated in Collective Works;
+ 4. to distribute copies or phonorecords of, display publicly, perform
+ publicly, and perform publicly by means of a digital audio
+ transmission Derivative Works.
+ 5.
+
+ For the avoidance of doubt, where the work is a musical composition:
+
+ 1. *Performance Royalties Under Blanket Licenses*. Licensor
+ waives the exclusive right to collect, whether individually
+ or via a performance rights society (e.g. ASCAP, BMI,
+ SESAC), royalties for the public performance or public
+ digital performance (e.g. webcast) of the Work.
+ 2. *Mechanical Rights and Statutory Royalties*. Licensor waives
+ the exclusive right to collect, whether individually or via
+ a music rights agency or designated agent (e.g. Harry Fox
+ Agency), royalties for any phonorecord You create from the
+ Work ("cover version") and distribute, subject to the
+ compulsory license created by 17 USC Section 115 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+ 6. *Webcasting Rights and Statutory Royalties*. For the avoidance of
+ doubt, where the Work is a sound recording, Licensor waives the
+ exclusive right to collect, whether individually or via a
+ performance-rights society (e.g. SoundExchange), royalties for the
+ public digital performance (e.g. webcast) of the Work, subject to
+ the compulsory license created by 17 USC Section 114 of the US
+ Copyright Act (or the equivalent in other jurisdictions).
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights
+in other media and formats. All rights not expressly granted by Licensor
+are hereby reserved.
+
+*4. Restrictions.*The license granted in Section 3 above is expressly
+made subject to and limited by the following restrictions:
+
+ 1. You may distribute, publicly display, publicly perform, or
+ publicly digitally perform the Work only under the terms of this
+ License, and You must include a copy of, or the Uniform Resource
+ Identifier for, this License with every copy or phonorecord of the
+ Work You distribute, publicly display, publicly perform, or
+ publicly digitally perform. You may not offer or impose any terms
+ on the Work that alter or restrict the terms of this License or
+ the recipients' exercise of the rights granted hereunder. You may
+ not sublicense the Work. You must keep intact all notices that
+ refer to this License and to the disclaimer of warranties. You may
+ not distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work with any technological measures that
+ control access or use of the Work in a manner inconsistent with
+ the terms of this License Agreement. The above applies to the Work
+ as incorporated in a Collective Work, but this does not require
+ the Collective Work apart from the Work itself to be made subject
+ to the terms of this License. If You create a Collective Work,
+ upon notice from any Licensor You must, to the extent practicable,
+ remove from the Collective Work any credit as required by clause
+ 4(b), as requested. If You create a Derivative Work, upon notice
+ from any Licensor You must, to the extent practicable, remove from
+ the Derivative Work any credit as required by clause 4(b), as
+ requested.
+ 2. If you distribute, publicly display, publicly perform, or publicly
+ digitally perform the Work or any Derivative Works or Collective
+ Works, You must keep intact all copyright notices for the Work and
+ provide, reasonable to the medium or means You are utilizing: (i)
+ the name of the Original Author (or pseudonym, if applicable) if
+ supplied, and/or (ii) if the Original Author and/or Licensor
+ designate another party or parties (e.g. a sponsor institute,
+ publishing entity, journal) for attribution in Licensor's
+ copyright notice, terms of service or by other reasonable means,
+ the name of such party or parties; the title of the Work if
+ supplied; to the extent reasonably practicable, the Uniform
+ Resource Identifier, if any, that Licensor specifies to be
+ associated with the Work, unless such URI does not refer to the
+ copyright notice or licensing information for the Work; and in the
+ case of a Derivative Work, a credit identifying the use of the
+ Work in the Derivative Work (e.g., "French translation of the Work
+ by Original Author," or "Screenplay based on original Work by
+ Original Author"). Such credit may be implemented in any
+ reasonable manner; provided, however, that in the case of a
+ Derivative Work or Collective Work, at a minimum such credit will
+ appear where any other comparable authorship credit appears and in
+ a manner at least as prominent as such other comparable authorship
+ credit.
+
+*5. Representations, Warranties and Disclaimer*
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE
+EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+*6. Limitation on Liability.* EXCEPT TO THE EXTENT REQUIRED BY
+APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL
+THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY
+DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF
+LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+*7. Termination*
+
+ 1. This License and the rights granted hereunder will terminate
+ automatically upon any breach by You of the terms of this License.
+ Individuals or entities who have received Derivative Works or
+ Collective Works from You under this License, however, will not
+ have their licenses terminated provided such individuals or
+ entities remain in full compliance with those licenses. Sections
+ 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
+ 2. Subject to the above terms and conditions, the license granted
+ here is perpetual (for the duration of the applicable copyright in
+ the Work). Notwithstanding the above, Licensor reserves the right
+ to release the Work under different license terms or to stop
+ distributing the Work at any time; provided, however that any such
+ election will not serve to withdraw this License (or any other
+ license that has been, or is required to be, granted under the
+ terms of this License), and this License will continue in full
+ force and effect unless terminated as stated above.
+
+*8. Miscellaneous*
+
+ 1. Each time You distribute or publicly digitally perform the Work or
+ a Collective Work, the Licensor offers to the recipient a license
+ to the Work on the same terms and conditions as the license
+ granted to You under this License.
+ 2. Each time You distribute or publicly digitally perform a
+ Derivative Work, Licensor offers to the recipient a license to the
+ original Work on the same terms and conditions as the license
+ granted to You under this License.
+ 3. If any provision of this License is invalid or unenforceable under
+ applicable law, it shall not affect the validity or enforceability
+ of the remainder of the terms of this License, and without further
+ action by the parties to this agreement, such provision shall be
+ reformed to the minimum extent necessary to make such provision
+ valid and enforceable.
+ 4. No term or provision of this License shall be deemed waived and no
+ breach consented to unless such waiver or consent shall be in
+ writing and signed by the party to be charged with such waiver or
+ consent.
+ 5. This License constitutes the entire agreement between the parties
+ with respect to the Work licensed here. There are no
+ understandings, agreements or representations with respect to the
+ Work not specified here. Licensor shall not be bound by any
+ additional provisions that may appear in any communication from
+ You. This License may not be modified without the mutual written
+ agreement of the Licensor and You.
+
+Creative Commons is not a party to this License, and makes no warranty
+whatsoever in connection with the Work. Creative Commons will not be
+liable to You or any party on any legal theory for any damages
+whatsoever, including without limitation any general, special,
+incidental or consequential damages arising in connection to this
+license. Notwithstanding the foregoing two (2) sentences, if Creative
+Commons has expressly identified itself as the Licensor hereunder, it
+shall have all rights and obligations of Licensor.
+
+Except for the limited purpose of indicating to the public that the Work
+is licensed under the CCPL, neither party will use the trademark
+"Creative Commons" or any related trademark or logo of Creative Commons
+without the prior written consent of Creative Commons. Any permitted use
+will be in compliance with Creative Commons' then-current trademark
+usage guidelines, as may be published on its website or otherwise made
+available upon request from time to time.
+
+Creative Commons may be contacted at http://creativecommons.org/
+<http://creativecommons.org>.
+
+« Back to Commons Deed <./>
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/templates/view.pt b/docs/tutorials/bfgwiki/src/views/tutorial/templates/view.pt
new file mode 100644
index 000000000..5326e6454
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/templates/view.pt
@@ -0,0 +1,26 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
+
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
+ <title>${page.__name__} - bfg tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+ <link rel="stylesheet" type="text/css"
+ href="${request.application_url}/static/style.css" />
+</head>
+
+<body>
+
+<div class="main_content">
+<div style="float:right; width: 10em;"> Viewing
+<span tal:replace="page.__name__">Page Name Goes Here</span> <br/>
+You can return to the <a href="${request.application_url}">FrontPage</a>.
+</div>
+
+<div tal:replace="structure content">Page text goes here.</div>
+<p><a tal:attributes="href edit_url" href="">Edit this page</a></p>
+</div>
+
+</body></html>
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/tests.py b/docs/tutorials/bfgwiki/src/views/tutorial/tests.py
new file mode 100644
index 000000000..84a3a0f37
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/tests.py
@@ -0,0 +1,150 @@
+import unittest
+
+from repoze.bfg import testing
+
+class PageModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Page
+ return Page
+
+ def _makeOne(self, data=u'some data'):
+ return self._getTargetClass()(data=data)
+
+ def test_constructor(self):
+ instance = self._makeOne()
+ self.assertEqual(instance.data, u'some data')
+
+class WikiModelTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from tutorial.models import Wiki
+ return Wiki
+
+ def _makeOne(self):
+ return self._getTargetClass()()
+
+ def test_it(self):
+ wiki = self._makeOne()
+ self.assertEqual(wiki.__parent__, None)
+ self.assertEqual(wiki.__name__, None)
+
+class AppmakerTests(unittest.TestCase):
+ def _callFUT(self, zodb_root):
+ from tutorial.models import appmaker
+ return appmaker(zodb_root)
+
+ def test_it(self):
+ root = {}
+ self._callFUT(root)
+ self.assertEqual(root['app_root']['FrontPage'].data,
+ 'This is the front page')
+
+class ViewWikiTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def test_it(self):
+ from tutorial.views import view_wiki
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ response = view_wiki(context, request)
+ self.assertEqual(response.location, 'http://example.com/FrontPage')
+
+class ViewPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import view_page
+ return view_page(context, request)
+
+ def test_it(self):
+ wiki = testing.DummyModel()
+ wiki['IDoExist'] = testing.DummyModel()
+ context = testing.DummyModel(data='Hello CruelWorld IDoExist')
+ context.__parent__ = wiki
+ context.__name__ = 'thepage'
+ request = testing.DummyRequest()
+ renderer = testing.registerDummyRenderer('templates/view.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(
+ renderer.content,
+ '<div class="document">\n'
+ '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
+ 'CruelWorld</a> '
+ '<a href="http://example.com/IDoExist/">'
+ 'IDoExist</a>'
+ '</p>\n</div>\n')
+ self.assertEqual(renderer.edit_url,
+ 'http://example.com/thepage/edit_page')
+
+
+class AddPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import add_page
+ return add_page(context, request)
+
+ def test_it_notsubmitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ request.subpath = ['AnotherPage']
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(renderer.page.data, '')
+
+ def test_it_submitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ request.subpath = ['AnotherPage']
+ response = self._callFUT(context, request)
+ page = context['AnotherPage']
+ self.assertEqual(page.data, 'Hello yo!')
+ self.assertEqual(page.__name__, 'AnotherPage')
+ self.assertEqual(page.__parent__, context)
+
+class EditPageTests(unittest.TestCase):
+ def setUp(self):
+ testing.cleanUp()
+
+ def tearDown(self):
+ testing.cleanUp()
+
+ def _callFUT(self, context, request):
+ from tutorial.views import edit_page
+ return edit_page(context, request)
+
+ def test_it_notsubmitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest()
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(renderer.request, request)
+ self.assertEqual(renderer.page, context)
+
+ def test_it_submitted(self):
+ context = testing.DummyModel()
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ renderer = testing.registerDummyRenderer('templates/edit.pt')
+ response = self._callFUT(context, request)
+ self.assertEqual(response.location, 'http://example.com/')
+ self.assertEqual(context.data, 'Hello yo!')
+
+
+
diff --git a/docs/tutorials/bfgwiki/src/views/tutorial/views.py b/docs/tutorials/bfgwiki/src/views/tutorial/views.py
new file mode 100644
index 000000000..eb1f9c0bf
--- /dev/null
+++ b/docs/tutorials/bfgwiki/src/views/tutorial/views.py
@@ -0,0 +1,71 @@
+from docutils.core import publish_parts
+import re
+
+from webob.exc import HTTPFound
+from repoze.bfg.url import model_url
+from repoze.bfg.chameleon_zpt import render_template_to_response
+from repoze.bfg.view import static
+
+from tutorial.models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+static_view = static('templates/static')
+
+def view_wiki(context, request):
+ return HTTPFound(location = model_url(context, request, 'FrontPage'))
+
+def view_page(context, request):
+ wiki = context.__parent__
+
+ def check(match):
+ word = match.group(1)
+ if word in wiki:
+ page = wiki[word]
+ view_url = model_url(page, request)
+ return '<a href="%s">%s</a>' % (view_url, word)
+ else:
+ add_url = request.application_url + '/add_page/' + word
+ return '<a href="%s">%s</a>' % (add_url, word)
+
+ content = publish_parts(context.data, writer_name='html')['html_body']
+ content = wikiwords.sub(check, content)
+ edit_url = model_url(context, request, 'edit_page')
+ return render_template_to_response('templates/view.pt',
+ request = request,
+ page = context,
+ content = content,
+ edit_url = edit_url)
+
+def add_page(context, request):
+ name = request.subpath[0]
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(body)
+ page.__name__ = name
+ page.__parent__ = context
+ context[name] = page
+ return HTTPFound(location = model_url(page, request))
+ save_url = model_url(context, request, 'add_page', name)
+ page = Page('')
+ page.__name__ = name
+ page.__parent__ = context
+ return render_template_to_response('templates/edit.pt',
+ request = request,
+ page = page,
+ save_url = save_url)
+
+def edit_page(context, request):
+ if 'form.submitted' in request.params:
+ context.data = request.params['body']
+ return HTTPFound(location = model_url(context, request))
+
+ return render_template_to_response('templates/edit.pt',
+ request = request,
+ page = context,
+ save_url = model_url(context, request,
+ 'edit_page')
+ )
+
+
diff --git a/docs/tutorials/bfgwiki/viewdecorators.rst b/docs/tutorials/bfgwiki/viewdecorators.rst
new file mode 100644
index 000000000..b9fd3b454
--- /dev/null
+++ b/docs/tutorials/bfgwiki/viewdecorators.rst
@@ -0,0 +1,257 @@
+==========================================================
+Using View Decorators Rather than ZCML ``view`` directives
+==========================================================
+
+So far we've been using :term:`ZCML` to map model types to views.
+It's often easier to use the "bfg_view" view decorator to do this
+mapping. Using view decorators provides better locality of reference
+for the mapping, because you can see which model types and view names
+the view will serve right next to the view function itself. In this
+mode, however, you lose the ability for some views to be overridden
+"from the outside" (by someone using your application as a framework).
+Since this application is *definitely* not a framework, it makes sense
+for us to switch over to using view decorators.
+
+Adding View Decorators
+======================
+
+We're going to import the ``bfg_view`` callable from the
+``repoze.bfg.view`` module. This callable can be used as a function
+decorator. We'll use it to decorate our ``static_view``,
+``view_wiki``, ``view_page``, ``add_page`` and ``edit_page`` view
+functions.
+
+The ``bfg_view`` callable accepts a number of arguments:
+
+``for_``
+
+ The model type which this view is "for", in our case a class.
+
+``name``
+
+ The name of the view.
+
+There are other arguments which this callable accepts, but these are
+the ones we're going to use.
+
+The ``static_view`` view function
+---------------------------------
+
+Because our ``bfg_view`` decorator can only decorate view functions
+and classes (not instances), we rename our ``static_view`` to
+``static_app`` and create a new function named ``static_view`` which
+simply calls ``static_app`` with the context and request. We decorate
+the resulting ``static_view`` function with the following:
+
+.. code-block:: python
+ :linenos:
+
+ @bfg_view(for_=Wiki, name='static')
+
+This indicates that the view is "for" the Wiki class and has the
+view_name ``static``. After injecting this decorator, we can now
+*remove* the following from our ``configure.zcml`` file:
+
+.. code-block:: xml
+ :linenos:
+
+ <view
+ for=".models.Wiki"
+ view=".views.static_view"
+ name="static"
+ />
+
+Our decorator takes its place.
+
+The ``view_wiki`` view function
+-------------------------------
+
+The decorator above the ``view_wiki`` function will be:
+
+.. code-block:: python
+ :linenos:
+
+ @bfg_view(for_=Wiki)
+
+This indicates that the view is "for" the Wiki class and has the
+*empty* view_name (indicating the default view). After injecting this
+decorator, we can now *remove* the following from our
+``configure.zcml`` file:
+
+.. code-block:: xml
+ :linenos:
+
+ <view
+ for=".models.Wiki"
+ view=".views.view_wiki"
+ />
+
+Our new decorator takes its place.
+
+The ``view_page`` view function
+-------------------------------
+
+The decorator above the ``view_page`` function will be:
+
+.. code-block:: python
+ :linenos:
+
+ @bfg_view(for_=Page)
+
+This indicates that the view is "for" the Page class and has the
+*empty* view_name (indicating the default view). After injecting this
+decorator, we can now *remove* the following from our
+``configure.zcml`` file:
+
+.. code-block:: xml
+ :linenos:
+
+ <view
+ for=".models.Page"
+ view=".views.view_page"
+ />
+
+
+Our new decorator takes its place.
+
+The ``add_page`` view function
+------------------------------
+
+The decorator above the ``add_page`` function will be:
+
+.. code-block:: python
+ :linenos:
+
+ @bfg_view(for_=Wiki, name='add_page')
+
+This indicates that the view is "for" the Wiki class and has the
+``add_page`` view_name. After injecting this decorator, we can now
+*remove* the following from our ``configure.zcml`` file:
+
+.. code-block:: xml
+ :linenos:
+
+ <view
+ for=".models.Wiki"
+ name="add_page"
+ view=".views.add_page"
+ />
+
+Our new decorator takes its place.
+
+The ``edit_page`` view function
+-------------------------------
+
+The decorator above the ``edit_page`` function will be:
+
+.. code-block:: python
+ :linenos:
+
+ @bfg_view(for_=Page, name='edit_page')
+
+This indicates that the view is "for" the Page class and has the
+``edit_page`` view_name. After injecting this decorator, we can now
+*remove* the following from our ``configure.zcml`` file:
+
+.. code-block:: xml
+ :linenos:
+
+ <view
+ for=".models.Page"
+ name="edit_page"
+ view=".views.edit_page"
+ />
+
+Our new decorator takes its place.
+
+Adding a Scan Directive
+=======================
+
+In order for our decorators to be recognized, we must add a bit of
+boilerplate to our ``configure.zcml`` file. Add the following tag
+anywhere beneath the ``<include package="repoze.bfg.includes">`` tag
+but before the ending ``</configure>`` tag within ``configure.zcml``:
+
+.. code-block:: xml
+ :linenos:
+
+ <include package="repoze.bfg.includes" />
+ <scan package="."/>
+
+Viewing the Result of Our Edits to ``views.py``
+===============================================
+
+The result of all of our edits to ``views.py`` will leave it looking
+like this:
+
+.. literalinclude:: src/viewdecorators/tutorial/views.py
+ :linenos:
+ :language: python
+
+Viewing the Results of Our Edits to ``configure.zcml``
+======================================================
+
+The result of all of our edits to ``configure.zcml`` will leave it
+looking like this:
+
+.. literalinclude:: src/viewdecorators/tutorial/configure.zcml
+ :linenos:
+ :language: xml
+
+Running the Tests
+=================
+
+We can run these tests by using ``setup.py test`` in the same way we
+did in :ref:`running_tests`. Assuming our shell's current working
+directory is the "tutorial" distribution directory:
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ ../bin/python setup.py test -q
+
+On Windows:
+
+.. code-block:: bash
+
+
+ c:\bigfntut\tutorial> ..\Scripts\python setup.py test -q
+
+Hopefully nothing will have changed. The expected result looks
+something like:
+
+.. code-block:: bash
+
+ .........
+ ----------------------------------------------------------------------
+ Ran 9 tests in 0.203s
+
+ OK
+
+Viewing the Application in a Browser
+====================================
+
+Once we've set up the WSGI pipeline properly, we can finally examine
+our application in a browser. We'll make sure that we didn't break
+any views by trying each of them.
+
+- Visiting `http://localhost:6543/ <http://localhost:6543/>`_ in a
+ browser invokes the ``view_wiki`` view. This always redirects to
+ the ``view_page`` view of the FrontPage page object.
+
+- Visiting `http://localhost:6543/FrontPage/
+ <http://localhost:6543/FrontPage/>`_ in a browser invokes the
+ ``view_page`` view of the front page page object. This is because
+ it's the *default view* (a view without a ``name``) for Page objects.
+
+- Visiting `http://localhost:6543/FrontPage/edit_page
+ <http://localhost:6543/FrontPage/edit_page>`_ in a browser invokes
+ the edit view for the front page object.
+
+- Visiting `http://localhost:6543/add_page/SomePageName
+ <http://localhost:6543/add_page/SomePageName>`_ in a browser invokes
+ the add view for a page.
+
+
+