summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2010-10-25 17:58:11 -0400
committerChris McDonough <chrism@plope.com>2010-10-25 17:58:11 -0400
commite26700528995b1807c5e55a4295a6f788a5603de (patch)
tree439e71c28d716bf5d5b742cee2578e6247da4dc3 /docs
parent4b679b4d7cf4a045293f9e652aee818cd8649dc3 (diff)
downloadpyramid-e26700528995b1807c5e55a4295a6f788a5603de.tar.gz
pyramid-e26700528995b1807c5e55a4295a6f788a5603de.tar.bz2
pyramid-e26700528995b1807c5e55a4295a6f788a5603de.zip
adjust wiki2 tutorial for pyramid
Diffstat (limited to 'docs')
-rw-r--r--docs/tutorials/wiki2/authorization.rst280
-rw-r--r--docs/tutorials/wiki2/background.rst17
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst148
-rw-r--r--docs/tutorials/wiki2/definingmodels.rst79
-rw-r--r--docs/tutorials/wiki2/definingviews.rst404
-rw-r--r--docs/tutorials/wiki2/distributing.rst49
-rw-r--r--docs/tutorials/wiki2/index.rst27
-rw-r--r--docs/tutorials/wiki2/installation.rst276
-rw-r--r--docs/tutorials/wiki2/src/authorization/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki2/src/authorization/README.txt4
-rw-r--r--docs/tutorials/wiki2/src/authorization/setup.cfg27
-rw-r--r--docs/tutorials/wiki2/src/authorization/setup.py46
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial.ini23
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/__init__.py2
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/configure.zcml65
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/login.py39
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models.py52
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/run.py25
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/security.py8
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt35
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt32
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/static/style.css109
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt31
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/tests.py139
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views.py71
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/README.txt4
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/setup.cfg27
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/setup.py45
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial.ini22
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/configure.zcml18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models.py45
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/run.py24
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py24
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views.py7
-rw-r--r--docs/tutorials/wiki2/src/models/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki2/src/models/README.txt4
-rw-r--r--docs/tutorials/wiki2/src/models/setup.cfg27
-rw-r--r--docs/tutorials/wiki2/src/models/setup.py45
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial.ini22
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/__init__.py2
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/configure.zcml18
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models.py43
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/run.py24
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/tests.py24
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views.py7
-rw-r--r--docs/tutorials/wiki2/src/views/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki2/src/views/README.txt4
-rw-r--r--docs/tutorials/wiki2/src/views/setup.cfg27
-rw-r--r--docs/tutorials/wiki2/src/views/setup.py46
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial.ini23
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/__init__.py2
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/configure.zcml38
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models.py43
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/run.py24
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt32
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt99
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/static/default.css380
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img01.gifbin0 -> 3840 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img02.gifbin0 -> 4689 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img03.gifbin0 -> 229 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img04.gifbin0 -> 92 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/static/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/static/style.css109
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/static/templatelicense.txt243
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/view.pt28
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/tests.py140
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views.py65
93 files changed, 5907 insertions, 0 deletions
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
new file mode 100644
index 000000000..6d8347f79
--- /dev/null
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -0,0 +1,280 @@
+.. _wiki2_adding_authorization:
+
+====================
+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 only 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:`pyramid` provides facilities for *authorization* and
+*authentication*. We'll make use of both features to provide security
+to our application.
+
+The source code for this tutorial stage can be browsed at
+`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/
+<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/>`_.
+
+Adding A Root Factory
+---------------------
+
+We're going to start to use a custom :term:`root factory` within our
+``run.py`` file. The objects generated by the root factory will be
+used as the :term:`context` of each request to our application. In
+order for :mod:`pyramid` declarative security to work properly, the
+context object generated during a request must be decorated with
+security declarations; when we begin to use a custom root factory to
+generate our contexts, we can begin to make use of the declarative
+security features of :mod:`pyramid`.
+
+Let's modify our ``run.py``, passing in a :term:`root factory` to our
+:term:`Configurator` constructor. We'll point it at a new class we
+create inside our ``models.py`` file. Add the following statements to
+your ``models.py`` file:
+
+.. code-block:: python
+
+ from pyramid.security import Allow
+ from pyramid.security import Everyone
+
+ class RootFactory(object):
+ __acl__ = [ (Allow, Everyone, 'view'),
+ (Allow, 'group:editors', 'edit') ]
+ def __init__(self, request):
+ self.__dict__.update(request.matchdict)
+
+The ``RootFactory`` class we've just added will be used by
+:mod:`pyramid` to construct a ``context`` object. The context is
+attached to the request object passed to our view callables as the
+``context`` attribute.
+
+All of our context objects will possess an ``__acl__`` attribute that
+allows :data:`pyramid.security.Everyone` (a special principal) to
+view all pages, while allowing only a :term:`principal` named
+``group:editors`` to edit and add pages. The ``__acl__`` attribute
+attached to a context is interpreted specially by :mod:`pyramid` as
+an access control list during view callable execution. See
+:ref:`assigning_acls` for more information about what an :term:`ACL`
+represents.
+
+.. note: Although we don't use the functionality here, the ``factory``
+ used to create route contexts may differ per-route as opposed to
+ globally. See the ``factory`` attribute in
+ :ref:`route_zcml_directive` for more info.
+
+We'll pass the ``RootFactory`` we created in the step above in as the
+``root_factory`` argument to a :term:`Configurator`. When we're done,
+your application's ``run.py`` will look like this.
+
+.. literalinclude:: src/authorization/tutorial/run.py
+ :linenos:
+ :language: python
+
+Configuring a ``pyramid`` Authorization Policy
+-------------------------------------------------
+
+For any :mod:`pyramid` application to perform authorization, we
+need to add a ``security.py`` module and we'll need to change our
+``configure.zcml`` file to add an :term:`authentication policy` and an
+:term:`authorization policy`.
+
+Changing ``configure.zcml``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We'll change our ``configure.zcml`` file to enable an
+``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to
+enable declarative security checking. We'll also change
+``configure.zcml`` to add a view stanza which points at our ``login``
+:term:`view callable`, also known as a :term:`forbidden view`. This
+configures our newly created login view to show up when
+:mod:`pyramid` detects that a view invocation can not be
+authorized. Also, we'll add ``view_permission`` attributes with the
+value ``edit`` to the ``edit_page`` and ``add_page`` route
+declarations. This indicates that the view callables which these
+routes reference cannot be invoked without the authenticated user
+possessing the ``edit`` permission with respect to the current
+context.
+
+This makes the assertion that only users who possess the effective
+``edit`` permission at the time of the request may invoke those two
+views. We've granted the ``group:editors`` principal the ``edit``
+permission at the root model via its ACL, so only the a user whom is a
+member of the group named ``group:editors`` will able to invoke the
+views associated with the ``add_page`` or ``edit_page`` routes.
+
+When you're done, your ``configure.zcml`` will look like so
+
+.. literalinclude:: src/authorization/tutorial/configure.zcml
+ :linenos:
+ :language: xml
+
+Note that the ``authtktauthenticationpolicy`` tag has two attributes:
+``secret`` and ``callback``. ``secret`` is a string representing an
+encryption key used by the "authentication ticket" machinery
+represented by this policy: it is required. The ``callback`` is a
+string, representing a :term:`dotted Python name`, which points at the
+``groupfinder`` function in the current directory's ``security.py``
+file. We haven't added that module yet, but we're about to.
+
+Adding ``security.py``
+~~~~~~~~~~~~~~~~~~~~~~
+
+Add a ``security.py`` module within your package (in the same
+directory as "run.py", "views.py", etc) with the following content:
+
+.. literalinclude:: src/authorization/tutorial/security.py
+ :linenos:
+ :language: python
+
+The groupfinder defined here is an :term:`authentication policy`
+"callback"; it is a callable that accepts a userid and a request. If
+the userid exists in the system, the callback will return a sequence
+of group identifiers (or an empty sequence if the user isn't a member
+of any groups). If the userid *does not* exist in the system, the
+callback will return ``None``. In a production system, user and group
+data will most often come from a database, but here we use "dummy"
+data to represent user and groups sources. Note that the ``editor``
+user is a member of the ``group:editors`` group in our dummy group
+data (the ``GROUPS`` data structure).
+
+We've given the ``editor`` user membership to the ``group:editors`` by
+mapping him to this group in the ``GROUPS`` data structure (``GROUPS =
+{'editor':['group:editors']}``). Since the ``groupfinder`` function
+consults the ``GROUPS`` data structure, this will mean that, as a
+result of the ACL attached to the root returned by the root factory,
+and the permission associated with the ``add_page`` and ``edit_page``
+views, the ``editor`` user should be able to add and edit pages.
+
+Adding Login and Logout Views
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We'll add a ``login`` view callable which renders a login form and
+processes the post from the login form, checking credentials.
+
+We'll also add a ``logout`` view callable 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.
+
+We'll add a different file (for presentation convenience) to add login
+and logout view callables. Add a file named ``login.py`` to your
+application (in the same directory as ``views.py``) with the following
+content:
+
+.. literalinclude:: src/authorization/tutorial/login.py
+ :linenos:
+ :language: python
+
+Changing Existing Views
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Then we need to change each of our ``view_page``, ``edit_page`` and
+``add_page`` views in ``views.py`` to pass a "logged in" parameter to
+its template. We'll add something like this to each view body:
+
+.. ignore-next-block
+.. code-block:: python
+ :linenos:
+
+ from pyramid.security import authenticated_userid
+ logged_in = authenticated_userid(request)
+
+We'll then change the return value of these views to pass the
+`resulting `logged_in`` value to the template, e.g.:
+
+.. ignore-next-block
+.. code-block:: python
+ :linenos:
+
+ return dict(page = context,
+ content = content,
+ logged_in = logged_in,
+ edit_url = edit_url)
+
+Adding the ``login.pt`` Template
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add a ``login.pt`` template to your templates directory. It's
+referred to within the login view we just added to ``login.py``.
+
+.. literalinclude:: src/authorization/tutorial/templates/login.pt
+ :linenos:
+ :language: xml
+
+Change ``view.pt`` and ``edit.pt``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We'll also need to change our ``edit.pt`` and ``view.pt`` templates 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>
+
+Viewing the Application in a Browser
+------------------------------------
+
+We can finally examine our application in a browser. The views we'll
+try are as follows:
+
+- Visiting ``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`` in a browser invokes
+ the ``view_page`` view of the FrontPage page object.
+
+- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser
+ invokes the edit view for the FrontPage 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
+ display the edit page form.
+
+- Visiting ``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
+ display the edit page form.
+
+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/wiki2/background.rst b/docs/tutorials/wiki2/background.rst
new file mode 100644
index 000000000..de35472bc
--- /dev/null
+++ b/docs/tutorials/wiki2/background.rst
@@ -0,0 +1,17 @@
+==========
+Background
+==========
+
+This tutorial presents a :mod:`pyramid` application that uses
+technologies which will be familiar to someone with :term:`Pylons`
+experience. It uses :term:`SQLAlchemy` as a persistence mechanism and
+:term:`url dispatch` to map URLs to code. It can also be followed by
+people without any prior Python web framework experience.
+
+To code along with this 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.
+
+This tutorial is targeted at :mod:`pyramid` version 1.0.
+
+Have fun!
diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst
new file mode 100644
index 000000000..63175138c
--- /dev/null
+++ b/docs/tutorials/wiki2/basiclayout.rst
@@ -0,0 +1,148 @@
+============
+Basic Layout
+============
+
+The starter files generated by the ``pyramid_routesalchemy`` template
+are basic, but they provide a good orientation for the high-level
+patterns common to most :term:`url dispatch` -based :mod:`pyramid`
+projects.
+
+The source code for this tutorial stage can be browsed at
+`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/basiclayout/
+<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/basiclayout/>`_.
+
+``__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``
+--------------------------------------
+
+:mod:`pyramid` uses a configuration 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.
+
+#. *Lines 3-4*. Boilerplate, the comment explains.
+
+#. *Lines 6-11*. Register a ``<route>`` :term:`route configuration`
+ that will be used when the URL is ``/``. Since this ``<route>``
+ has an empty ``pattern`` attribute, it is the "default" route. The
+ attribute named ``view`` with the value ``.views.my_view`` is the
+ dotted name to a *function* we write (generated by the
+ ``pyramid_routesalchemy`` template) that is given a ``request``
+ object and which returns a response or a dictionary. You will use
+ mostly ``<route>`` statements in a :term:`URL dispatch` based
+ application to map URLs to code. This ``route`` also names a
+ ``view_renderer``, which is a template which lives in the
+ ``templates`` subdirectory of the package. When the
+ ``.views.my_view`` view returns a dictionary, a :term:`renderer`
+ will use this template to create a response.
+
+#. *Lines 13-16*. Register a ``<static>`` directive that will match
+ any URL that starts with ``/static/``. This will serve up static
+ resources for us, in this case, at
+ ``http://localhost:6543/static/`` and below. With this
+ declaration, we're saying that any URL that starts with ``/static``
+ should go to the static view; any remainder of its path (e.g. the
+ ``/foo`` in ``/static/foo``) will be used to compose a path to a
+ static file resource, such as a CSS file.
+
+#. *Line 18*. The closing ``</configure>`` tag.
+
+Content Models with ``models.py``
+---------------------------------
+
+In a SQLAlchemy-based application, a *model* object is an object
+composed by querying the SQL database which backs an application.
+SQLAlchemy is an "object relational mapper" (an ORM). The
+``models.py`` file is where the ``pyramid_routesalchemy`` 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 1-14*. Imports to support later code.
+
+#. *Line 16*. We set up a SQLAlchemy "DBSession" object here. We
+ specify that we'd like to use the "ZopeTransactionExtension". This
+ extension is an extension which allows us to use a *transaction
+ manager* instead of controlling commits and aborts to database
+ operations by hand.
+
+#. *Line 17*. We create a declarative ``Base`` object to use as a
+ base class for our model.
+
+#. *Lines 19-27*. A model class named ``MyModel``. It has an
+ ``__init__`` that takes a two arguments (``name``, and ``value``).
+ It stores these values as ``self.name`` and ``self.value`` within
+ the ``__init__`` function itself. The ``MyModel`` class also has a
+ ``__tablename__`` attribute. This informs SQLAlchemy which table
+ to use to store the data representing instances of this class.
+
+#. *Lines 29-34*. A function named ``populate`` which adds a single
+ model instance into our SQL storage and commits a transaction.
+
+#. *Lines 36-44*. A function named ``initialize_sql`` which sets up
+ an actual SQL database and binds it to our SQLAlchemy DBSession
+ object. It also calls the ``populate`` function, to do initial
+ database population.
+
+App Startup with ``run.py``
+---------------------------
+
+When you run the application using the ``paster`` command using the
+``tutorial.ini`` generated config file, the application configuration
+points at an Setuptools *entry point* described as
+``egg:tutorial#app``. In our application, because the application's
+``setup.py`` file says so, this entry point happens to be the ``app``
+function within the file named ``run.py``:
+
+ .. literalinclude:: src/basiclayout/tutorial/run.py
+ :linenos:
+ :language: py
+
+#. *Lines 1-3*. Imports to support later code.
+
+#. *Line 12*. Obtain the ``configure_zcml`` setting from a value in
+ the ``tutorial.ini`` file's ``[app:sqlalchemy]`` section. If it
+ doesn't exist in the configuration file, default to
+ ``configure.zcml``.
+
+#. *Lines 13-15*. Get the database configuration string from the
+ ``tutorial.ini`` file's ``[app:sqlalchemy]`` section. This will be a URI
+ (something like ``sqlite://``).
+
+#. *Line 16*. Get the database echo settingf rom ``tutorial.ini``
+ file's ``[app:sqlalchemy]`` section. This will either be ``true``
+ or ``false``. If ``true``, the application will print SQL to the
+ console as it is generated and run by SQLAlchemy. By default, it
+ is false.
+
+#. Line *17*. We initialize our SQL database using SQLAlchemy, passing
+ it the db string and a variant of the db_echo value.
+
+#. *Line 18*. We construct a :term:`Configurator`. ``settings`` is
+ passed as a keyword argument with the dictionary values passed by
+ PasteDeploy as the ``settings`` argument. This will be a
+ dictionary of settings parsed by PasteDeploy, which contains
+ deployment-related values such as ``reload_templates``,
+ ``db_string``, etc.
+
+#. *Lines 19-22*. We then load a ZCML file to do application
+ configuration, and use the
+ :meth:`pyramid.configuration.Configurator.make_wsgi_app` method
+ to return a :term:`WSGI` application.
+
diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst
new file mode 100644
index 000000000..18d8363ad
--- /dev/null
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -0,0 +1,79 @@
+===============
+Defining Models
+===============
+
+The first change we'll make to our stock paster-generated application
+will be to define a :term:`model` constructor representing a wiki
+page. We'll do this inside our ``models.py`` file.
+
+The source code for this tutorial stage can be browsed at
+`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/models/
+<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/models/>`_.
+
+Making Edits to ``models.py``
+-----------------------------
+
+.. 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.
+
+The first thing we want to do is remove the stock ``Model`` class from
+the generated ``models.py`` file. The ``Model`` class is only a
+sample and we're not going to use it.
+
+Then, we'll add a ``Page`` class. Because this is a SQLAlchemy
+application, this class should inherit from an instance of
+:class:`sqlalchemy.ext.declarative.declarative_base`. Declarative
+SQLAlchemy models are easier to use than directly-mapped ones. The
+code generated by our ``routesalchemy`` paster template does not use
+declarative SQLAlchemy syntax, so we'll need to change various things
+to begin to use declarative syntax.
+
+Our ``Page`` class will have a class level attribute ``__tablename__``
+which equals the string ``pages``. This means that SQLAlchemy will
+store our wiki data in a SQL table named ``pages``. Our Page class
+will also have class-level attributes named ``id``, ``pagename`` and
+``data`` (all instances of :class:`sqlalchemy.Column`). These will
+map to columns in the ``pages`` table. The ``id`` attribute will be
+the primary key in the table. The ``name`` attribute will be a text
+attribute, each value of which needs to be unique within the column.
+The ``data`` attribute is a text attribute that will hold the body of
+each page.
+
+We'll also remove our ``populate`` function. We'll inline the
+populate step into ``initialize_sql``, changing our ``initialize_sql``
+function to add a FrontPage object to our database at startup time.
+We're also going to use slightly different binding syntax. It will
+will otherwise largely be the same as the ``initialize_sql`` in the
+paster-generated ``models.py``.
+
+Our DBSession assignment stays the same as the original generated
+``models.py``.
+
+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
+
+Viewing the Application in a Browser
+------------------------------------
+
+We can't. At this point, our system is in a "non-runnable" state;
+we'll need to change view-related files in the next chapter to be able
+to start the application successfully. If you try to start the
+application, you'll wind up with a Python traceback on your console
+that ends with this exception:
+
+.. code-block:: text
+
+ ImportError: cannot import name MyModel
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
new file mode 100644
index 000000000..1bba7efea
--- /dev/null
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -0,0 +1,404 @@
+==============
+Defining Views
+==============
+
+A :term:`view callable` in a :term:`url dispatch` -based
+:mod:`pyramid` application is typically a simple Python function that
+accepts a single parameter named :term:`request`. A view callable is
+assumed to return a :term:`response` object.
+
+.. note:: A :mod:`pyramid` view can also be defined as callable
+ which accepts *two* arguments: a :term:`context` and a
+ :term:`request`. You'll see this two-argument pattern used in
+ other :mod:`pyramid` tutorials and applications. Either calling
+ convention will work in any :mod:`pyramid` application; the
+ calling conventions can be used interchangeably as necessary. In
+ :term:`url dispatch` based applications, however, the context
+ object is rarely used in the view body itself, so within this
+ tutorial we define views as callables that accept only a request to
+ avoid the visual "noise". If you do need the ``context`` within a
+ view function that only takes the request as a single argument, you
+ can obtain it via ``request.context``.
+
+The request passed to every view that is called as the result of a
+route match has an attribute named ``matchdict`` that contains the
+elements placed into the URL by the ``pattern`` of a ``route``
+statement. For instance, if a route statement in ``configure.zcml``
+had the pattern ``:one/:two``, and the URL at
+``http://example.com/foo/bar`` was invoked, matching this pattern, the
+matchdict dictionary attached to the request passed to the view would
+have a ``one`` key with the value ``foo`` and a ``two`` key with the
+value ``bar``.
+
+The source code for this tutorial stage can be browsed at
+`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/views/
+<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/views/>`_.
+
+Declaring Dependencies in Our ``setup.py`` File
+===============================================
+
+The view code in our application will depend on a package which is not
+a dependency of the original "tutorial" application. The original
+"tutorial" application was generated by the ``paster create`` command;
+it doesn't know about our custom application requirements. We need to
+add a dependency on the ``docutils`` package to our ``tutorial``
+package's ``setup.py`` file by assigning this dependency to the
+``install_requires`` parameter in the ``setup`` function.
+
+Our resulting ``setup.py`` should look like so:
+
+.. literalinclude:: src/views/setup.py
+ :linenos:
+ :language: python
+
+.. note:: After these new dependencies are added, you will need to
+ rerun ``python setup.py develop`` inside the root of the
+ ``tutorial`` package to obtain and register the newly added
+ dependency package.
+
+Adding View Functions
+=====================
+
+We'll get rid of our ``my_view`` view function in our ``views.py``
+file. It's only an example and isn't relevant to our application.
+
+Then we're going to add four :term:`view callable` functions to our
+``views.py`` module. One view callable (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 callable
+named ``edit_page`` will allow a page to be edited. We'll describe
+each one briefly and show the resulting ``views.py`` file afterward.
+
+.. note::
+
+ There is nothing special about the filename ``views.py``. A project
+ may have many view callables throughout its codebase in
+ arbitrarily-named files. Files implementing view callables 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 :term:`default view` of
+a ``Wiki`` model object. It always redirects to a URL which
+represents the path to our "FrontPage". It returns an instance of the
+:class:`webob.exc.HTTPFound` class (instances of which implement the
+WebOb :term:`response` interface), It will use the
+:func:`pyramid.url.route_url` API to construct a URL to the
+``FrontPage`` page (e.g. ``http://localhost:6543/FrontPage``), and
+will use 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 :term:`default view` of
+a ``Page`` object. The ``view_page`` function renders the
+:term:`ReStructuredText` body of a page (stored as the ``data``
+attribute of 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
+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
+substitution 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 return a dictionary with a number of arguments.
+The fact that this view returns a dictionary (as opposed to a
+:term:`response` object) is a cue to :mod:`pyramid` that it should
+try to use a :term:`renderer` associated with the view configuration
+to render a template. In our case, the template which will be
+rendered will be the ``templates/view.pt`` template, as per the
+configuration put into effect in ``configure.zcml``.
+
+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 ``matchdict`` attribute of the
+request passed to the ``add_page`` view will have the values we need
+to construct URLs and find model objects.
+
+The matchdict will have a ``pagename`` key that matches the name of
+the page we'd like to add. If our add view is invoked via,
+e.g. ``http://localhost:6543/add_page/SomeName``, the ``pagename``
+value in the matchdict will be ``SomeName``.
+
+If the view execution is *not* a result of a form submission (if the
+expression ``'form.submitted' in request.params`` is ``False``), the
+view callable 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
+:mod:`pyramid` will render the template associated with this view
+to a response.
+
+If the view execution *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 matchdict ``pagename``, and obtain the page body from
+the request, and save it into the database using ``session.add``. We
+then redirect back to the ``view_page`` view (the :term:`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
+``matchdict`` attribute of the request passed to the ``add_page`` view
+will have a ``pagename`` key matching the name of the page the user
+wants to edit.
+
+If the view execution 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 execution *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`` key in the matchdict. It then redirects to the
+default view of the wiki 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:`pyramid` is a variant of :term:`ZPT` provided by
+:term:`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
+
+.. note:: The names available for our use in a template are always
+ those that are present in the dictionary returned by the view
+ callable. But our templates make use of a ``request`` object that
+ none of our tutorial views return in their dictionary. This value
+ appears as if "by magic". However, ``request`` is one of several
+ names that are available "by default" in a template when a template
+ renderer is used. See :ref:`chameleon_template_renderers` for more
+ information about other names that are available by default in a
+ template when a Chameleon template is used as a renderer.
+
+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. This file is a little
+too long to replicate within the body of this guide, however it is
+available `online
+<http://github.com/Pylons/pyramid/blob/master/docs/tutorials/wiki2/src/views/tutorial/templates/static/style.css>`_.
+
+
+This CSS file will be accessed via
+e.g. ``http://localhost:6543/static/style.css`` by virtue of the
+``<static>`` directive we've defined in the ``configure.zcml`` 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.
+
+Mapping Views to URLs in ``configure.zcml``
+===========================================
+
+The ``configure.zcml`` file contains ``route`` declarations (and a
+lone ``view`` declaration) which serve to map URLs via :term:`url
+dispatch` to view functions. First, we’ll get rid of the existing
+``route`` created by the template using the name ``home``. It’s only
+an example and isn’t relevant to our application.
+
+We then need to add four ``route`` declarations to ``configure.zcml``.
+Note that the *ordering* of these declarations is very important.
+``route`` declarations are matched in the order they're found in the
+``configure.zcml`` file.
+
+#. Add a declaration which maps the empty pattern (signifying the root
+ URL) to the view named ``view_wiki`` in our ``views.py`` file with
+ the name ``view_wiki``. This is the :term:`default view` for the
+ wiki.
+
+#. Add a declaration which maps the pattern ``:pagename`` to the
+ view named ``view_page`` in our ``views.py`` file with the view
+ name ``view_page``. This is the regular view for a page.
+
+#. Add a declaration which maps the pattern
+ ``:pagename/edit_page`` to the view named ``edit_page`` in our
+ ``views.py`` file with the name ``edit_page``. This is the edit view
+ for a page.
+
+#. Add a declaration which maps the pattern
+ ``add_page/:pagename`` to the view named ``add_page`` in our
+ ``views.py`` file with the name ``add_page``. This is the add view
+ for a new 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
+
+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.tm2#tm`` is at the "top" of the pipeline. This is a
+piece of middleware which commits a transaction if no exception
+occurs; if an exception occurs, the transaction will be aborted. This
+is the piece of software that allows us to forget about needing to do
+manual commits and aborts of our database connection in view code.
+
+Adding an Element to the Pipeline
+---------------------------------
+
+Let's add a piece of middleware to the WSGI pipeline. We'll add
+``egg:Paste#evalerror`` middleware which displays debuggable errors in
+the browser while you're developing (this is *not* recommended for
+deployment as it is a security risk). Let's insert evalerror into the
+pipeline right above ``egg:repoze.tm2#tm``, 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`` 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`` in a browser invokes
+ the ``view_page`` view of the front page page object.
+
+- Visiting ``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`` in a
+ browser invokes the add view for a page.
+
+Try generating an error within the body of a view by adding code to
+the top of it that generates an exception (e.g. ``raise
+Exception('Forced Exception')``). Then visit the error-raising view
+in a browser. You should see an interactive exception handler in the
+browser which allows you to examine values in a post-mortem mode.
+
+Adding Tests
+============
+
+Since we've added a good bit of imperative code here, it's useful to
+define tests for the views we've created. We'll change our tests.py
+module to look like this:
+
+.. literalinclude:: src/views/tutorial/tests.py
+ :linenos:
+ :language: python
+
+We can then run the tests using something like:
+
+.. code-block:: text
+ :linenos:
+
+ $ python setup.py test -q
+
+The expected output is something like:
+
+.. code-block:: text
+ :linenos:
+
+ running test
+ 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
+ unrecognized .svn/entries format in
+ reading manifest file 'tutorial.egg-info/SOURCES.txt'
+ writing manifest file 'tutorial.egg-info/SOURCES.txt'
+ running build_ext
+ ......
+ ----------------------------------------------------------------------
+ Ran 6 tests in 0.181s
+
+ OK
+
+
+
+
diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst
new file mode 100644
index 000000000..44e0291d1
--- /dev/null
+++ b/docs/tutorials/wiki2/distributing.rst
@@ -0,0 +1,49 @@
+=============================
+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 :mod:`pyramid` environment.
+
+On UNIX:
+
+.. code-block:: text
+
+ $ ../bin/python setup.py sdist
+
+On Windows:
+
+.. code-block:: text
+
+ 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:: text
+
+ running sdist
+ # ... more output ...
+ 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/wiki2/index.rst b/docs/tutorials/wiki2/index.rst
new file mode 100644
index 000000000..655d14aed
--- /dev/null
+++ b/docs/tutorials/wiki2/index.rst
@@ -0,0 +1,27 @@
+.. _bfg_sql_wiki_tutorial:
+
+SQLAlchemy + URL Dispatch Wiki Tutorial
+=======================================
+
+This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`
+-based :mod:`pyramid` application to a developer familiar with
+Python. When the tutorial is finished, the developer will have
+created a basic Wiki application with authentication.
+
+For cut and paste purposes, the source code for all stages of this
+tutorial can be browsed at
+`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/
+<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/>`_.
+
+.. toctree::
+ :maxdepth: 2
+
+ background
+ installation
+ basiclayout
+ definingmodels
+ definingviews
+ authorization
+ distributing
+
+
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
new file mode 100644
index 000000000..ae2eb4d06
--- /dev/null
+++ b/docs/tutorials/wiki2/installation.rst
@@ -0,0 +1,276 @@
+============
+Installation
+============
+
+For the most part, the installation process for this tutorial
+duplicates the steps described in :ref:`installing_chapter` and
+:ref:`project_narr`, however it also explains how to install
+additional libraries for tutorial purposes.
+
+Preparation
+===========
+
+Please take the following steps to prepare for the tutorial. The
+steps are slightly different depending on whether you're using UNIX or
+Windows.
+
+Preparation, UNIX
+-----------------
+
+#. Install SQLite3 and its development packages if you don't already
+ have them installed. Usually this is via your system's package
+ manager. For example, on a Debian Linux system, do ``sudo apt-get
+ install libsqlite3-dev``.
+
+#. If you don't already have a Python 2.6 interpreter installed on
+ your system, obtain, install, or find `Python 2.6
+ <http://www.python.org/download/releases/2.6.6/>`_ for your system.
+
+#. Install the 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.6 installation:
+
+ .. code-block:: text
+
+ $ /path/to/my/Python-2.6/bin/python ez_setup.py
+
+#. Use that Python's `bin/easy_install` to install `virtualenv`:
+
+ .. code-block:: text
+
+ $ /path/to/my/Python-2.6/bin/easy_install virtualenv
+
+#. Use that Python's virtualenv to make a workspace:
+
+ .. code-block:: text
+
+ $ path/to/my/Python-2.6/bin/virtualenv --no-site-packages bigfntut
+
+#. Switch to the ``bigfntut`` directory:
+
+ .. code-block:: text
+
+ $ cd bigfntut
+
+#. (Optional) Consider using ``source bin/activate`` to make your
+ shell environment wired to use the virtualenv.
+
+#. Use ``easy_install`` to get :mod:`pyramid` and its direct
+ dependencies installed:
+
+ .. code-block:: text
+
+ $ bin/easy_install pyramid
+
+#. Use ``easy_install`` to install various packages from PyPI.
+
+ .. code-block:: text
+
+ $ bin/easy_install docutils nose coverage zope.sqlalchemy \
+ SQLAlchemy repoze.tm2
+
+Preparation, Windows
+--------------------
+
+#. Install, or find `Python 2.6.6
+ <http://python.org/download/releases/2.6.6/>`_ for your system.
+
+#. Install the 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.6 installation using a
+ command prompt:
+
+ .. code-block:: text
+
+ c:\> c:\Python26\python ez_setup.py
+
+#. Use that Python's `bin/easy_install` to install `virtualenv`:
+
+ .. code-block:: text
+
+ c:\> c:\Python26\Scripts\easy_install virtualenv
+
+#. Use that Python's virtualenv to make a workspace:
+
+ .. code-block:: text
+
+ c:\> c:\Python26\Scripts\virtualenv --no-site-packages bigfntut
+
+#. Switch to the ``bigfntut`` directory:
+
+ .. code-block:: text
+
+ c:\> cd bigfntut
+
+#. (Optional) Consider using ``bin\activate.bat`` to make your shell
+ environment wired to use the virtualenv.
+
+#. Use ``easy_install`` to get :mod:`pyramid` and its direct
+ dependencies installed:
+
+ .. code-block:: text
+
+ c:\bigfntut> Scripts\easy_install pyramid
+
+#. Use ``easy_install`` to install various packages from PyPI.
+
+ .. code-block:: text
+
+ c:\bigfntut> Scripts\easy_install -i docutils \
+ nose coverage zope.sqlalchemy SQLAlchemy repoze.tm2
+
+
+.. _sql_making_a_project:
+
+Making a Project
+================
+
+Your next step is to create a project. :mod:`pyramid` supplies a
+variety of templates to generate sample projects. We will use the
+``pyramid_routesalchemy`` template, which generates an application
+that uses :term:`SQLAlchemy` and :term:`URL dispatch`.
+
+The below instructions assume your current working directory is the
+"virtualenv" named "bigfntut".
+
+On UNIX:
+
+.. code-block:: text
+
+ $ bin/paster create -t pyramid_routesalchemy tutorial
+
+On Windows:
+
+.. code-block:: text
+
+ c:\bigfntut> Scripts\paster create -t pyramid_routesalchemy tutorial
+
+.. note:: If you are using Windows, the ``pyramid_routesalchemy``
+ Paster template may not deal gracefully with installation into a
+ location that contains spaces in the path. If you experience
+ startup problems, try putting both the virtualenv and the project
+ into directories that do not contain spaces in their paths.
+
+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:`sql_making_a_project`, and run the
+"setup.py develop" command using virtualenv Python interpreter.
+
+On UNIX:
+
+.. code-block:: text
+
+ $ cd tutorial
+ $ ../bin/python setup.py develop
+
+On Windows:
+
+.. code-block:: text
+
+ c:\bigfntut> cd tutorial
+ c:\bigfntut\tutorial> ..\Scripts\python setup.py develop
+
+.. _sql_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:: text
+
+ $ ../bin/python setup.py test -q
+
+On Windows:
+
+.. code-block:: text
+
+ c:\bigfntut\tutorial> ..\Scripts\python setup.py test -q
+
+Starting the Application
+========================
+
+Start the application.
+
+On UNIX:
+
+.. code-block:: text
+
+ $ ../bin/paster serve tutorial.ini --reload
+
+On Windows:
+
+.. code-block:: text
+
+ 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.
+
+To get this functionality working, we'll need to install a couple of
+other packages into our ``virtualenv``: ``nose`` and ``coverage``:
+
+On UNIX:
+
+.. code-block:: text
+
+ $ ../bin/easy_install nose coverage
+
+On Windows:
+
+.. code-block:: text
+
+ c:\bigfntut\tutorial> ..\Scripts\easy_install nose coverage
+
+Once ``nose`` and ``coverage`` are installed, we can actually run the
+coverage tests.
+
+On UNIX:
+
+.. code-block:: text
+
+ $ ../bin/nosetests --cover-package=tutorial --cover-erase --with-coverage
+
+On Windows:
+
+.. code-block:: text
+
+ c:\bigfntut\tutorial> ..\Scripts\nosetests --cover-package=tutorial \
+ --cover-erase --with-coverage
+
+Looks like our package's ``models`` module doesn't quite have 100%
+test coverage.
+
+Visit the Application in a Browser
+==================================
+
+In a browser, visit ``http://localhost:6543/``. You will see the
+generated application's default page.
+
+Decisions the ``pyramid_routesalchemy`` Template Has Made For You
+=================================================================
+
+Creating a project using the ``pyramid_routesalchemy`` template makes
+the assumption that you are willing to use :term:`SQLAlchemy` as a
+database access tool and :term:`url dispatch` to map URLs to code.
+:mod:`pyramid` supports any persistent storage mechanism (e.g. object
+database or filesystem files, etc). It also supports an additional
+mechanism to map URLs to code (:term:`traversal`). However, for the
+purposes of this tutorial, we'll only be using url dispatch and
+SQLAlchemy.
+
diff --git a/docs/tutorials/wiki2/src/authorization/CHANGES.txt b/docs/tutorials/wiki2/src/authorization/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/wiki2/src/authorization/setup.cfg b/docs/tutorials/wiki2/src/authorization/setup.cfg
new file mode 100644
index 000000000..23b2ad983
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/setup.cfg
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package=tutorial
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = tutorial/locale
+domain = tutorial
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = tutorial/locale/tutorial.pot
+width = 80
+
+[init_catalog]
+domain = tutorial
+input_file = tutorial/locale/tutorial.pot
+output_dir = tutorial/locale
+
+[update_catalog]
+domain = tutorial
+input_file = tutorial/locale/tutorial.pot
+output_dir = tutorial/locale
+previous = true
diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py
new file mode 100644
index 000000000..eeac5c397
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/setup.py
@@ -0,0 +1,46 @@
+import os
+import sys
+
+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 = [
+ 'pyramid',
+ 'SQLAlchemy',
+ 'transaction',
+ 'repoze.tm2',
+ 'zope.sqlalchemy',
+ 'docutils'
+ ]
+
+if sys.version_info[:3] < (2,5,0):
+ requires.append('pysqlite')
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pylons",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi pylons pyramid bfg',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ test_suite='tutorial',
+ install_requires = requires,
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial.ini b/docs/tutorials/wiki2/src/authorization/tutorial.ini
new file mode 100644
index 000000000..85f131c2e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+debug = true
+
+[app:sqlalchemy]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+debug_templates = true
+default_locale_name = en
+db_string = sqlite:///%(here)s/tutorial.db
+db_echo = false
+
+[pipeline:main]
+pipeline =
+ egg:Paste#evalerror
+ egg:repoze.tm2#tm
+ sqlalchemy
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/configure.zcml b/docs/tutorials/wiki2/src/authorization/tutorial/configure.zcml
new file mode 100644
index 000000000..03b0e5b5a
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/configure.zcml
@@ -0,0 +1,65 @@
+<configure xmlns="http://pylonshq.com/pyramid">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="pyramid.includes" />
+
+ <static
+ path="templates/static"
+ name="static"
+ />
+
+ <route
+ pattern="login"
+ name="login"
+ view=".login.login"
+ view_renderer="templates/login.pt"
+ />
+
+ <route
+ pattern="logout"
+ name="logout"
+ view=".login.logout"
+ />
+
+ <route
+ pattern=""
+ name="view_wiki"
+ view=".views.view_wiki"
+ />
+
+ <route
+ pattern=":pagename"
+ name="view_page"
+ view=".views.view_page"
+ view_renderer="templates/view.pt"
+ />
+
+ <route
+ pattern="add_page/:pagename"
+ name="add_page"
+ view=".views.add_page"
+ view_renderer="templates/edit.pt"
+ view_permission="edit"
+ />
+
+ <route
+ pattern=":pagename/edit_page"
+ name="edit_page"
+ view=".views.edit_page"
+ view_renderer="templates/edit.pt"
+ view_permission="edit"
+ />
+
+ <view
+ view=".login.login"
+ renderer="templates/login.pt"
+ for="pyramid.exceptions.Forbidden"/>
+
+ <authtktauthenticationpolicy
+ secret="sosecret"
+ callback=".security.groupfinder"
+ />
+
+ <aclauthorizationpolicy/>
+
+</configure>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/login.py b/docs/tutorials/wiki2/src/authorization/tutorial/login.py
new file mode 100644
index 000000000..1a54d575c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/login.py
@@ -0,0 +1,39 @@
+from webob.exc import HTTPFound
+
+from pyramid.security import remember
+from pyramid.security import forget
+from pyramid.url import route_url
+
+from tutorial.security import USERS
+
+def login(request):
+ login_url = route_url('login', request)
+ referrer = request.url
+ if referrer == login_url:
+ referrer = '/' # never use the login form itself as came_from
+ came_from = request.params.get('came_from', referrer)
+ message = ''
+ login = ''
+ password = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ if USERS.get(login) == password:
+ headers = remember(request, login)
+ return HTTPFound(location = came_from,
+ headers = headers)
+ message = 'Failed login'
+
+ return dict(
+ message = message,
+ url = request.application_url + '/login',
+ came_from = came_from,
+ login = login,
+ password = password,
+ )
+
+def logout(request):
+ headers = forget(request)
+ return HTTPFound(location = route_url('view_wiki', request),
+ headers = headers)
+
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models.py b/docs/tutorials/wiki2/src/authorization/tutorial/models.py
new file mode 100644
index 000000000..a77b4964c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models.py
@@ -0,0 +1,52 @@
+import transaction
+
+from pyramid.security import Allow
+from pyramid.security import Everyone
+
+from sqlalchemy import create_engine
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import Text
+
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.ext.declarative import declarative_base
+
+from sqlalchemy.orm import scoped_session
+from sqlalchemy.orm import sessionmaker
+
+from zope.sqlalchemy import ZopeTransactionExtension
+
+DBSession = scoped_session(sessionmaker(
+ extension=ZopeTransactionExtension()))
+Base = declarative_base()
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, unique=True)
+ data = Column(Text)
+
+ def __init__(self, name, data):
+ self.name = name
+ self.data = data
+
+def initialize_sql(db_string, echo=False):
+ engine = create_engine(db_string, echo=echo)
+ DBSession.configure(bind=engine)
+ Base.metadata.bind = engine
+ Base.metadata.create_all(engine)
+ try:
+ session = DBSession()
+ page = Page('FrontPage', 'initial data')
+ session.add(page)
+ transaction.commit()
+ except IntegrityError:
+ # already created
+ pass
+
+class RootFactory(object):
+ __acl__ = [ (Allow, Everyone, 'view'),
+ (Allow, 'group:editors', 'edit') ]
+ def __init__(self, request):
+ self.__dict__.update(request.matchdict)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/run.py b/docs/tutorials/wiki2/src/authorization/tutorial/run.py
new file mode 100644
index 000000000..82ce37490
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/run.py
@@ -0,0 +1,25 @@
+from pyramid.configuration import Configurator
+from paste.deploy.converters import asbool
+
+from tutorial.models import initialize_sql
+from tutorial.models import RootFactory
+
+def app(global_config, **settings):
+ """ This function returns a WSGI application.
+
+ It is usually called by the PasteDeploy framework during
+ ``paster serve``.
+ """
+ zcml_file = settings.get('configure_zcml', 'configure.zcml')
+ db_string = settings.get('db_string')
+ if db_string is None:
+ raise ValueError(
+ "No 'db_string' value in application configuration.")
+ db_echo = settings.get('db_echo', 'false')
+ initialize_sql(db_string, asbool(db_echo))
+ config = Configurator(settings=settings, root_factory=RootFactory)
+ config.begin()
+ config.load_zcml(zcml_file)
+ config.end()
+ return config.make_wsgi_app()
+
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
new file mode 100644
index 000000000..cfd13071e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
@@ -0,0 +1,8 @@
+USERS = {'editor':'editor',
+ 'viewer':'viewer'}
+GROUPS = {'editor':['group:editors']}
+
+def groupfinder(userid, request):
+ if userid in USERS:
+ return GROUPS.get(userid, [])
+
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt
new file mode 100644
index 000000000..05e2ecd76
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt
@@ -0,0 +1,35 @@
+<!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>Pyramid 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/wiki2/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt
new file mode 100644
index 000000000..c56983d64
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt
@@ -0,0 +1,32 @@
+<!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>Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+ <link rel="stylesheet" type="text/css"
+ href="${request.application_url}/static/style.css" />
+</head>
+
+<body>
+
+<h1>Log In</h1>
+
+<div tal:replace="message"/>
+
+<div class="main_content">
+ <form action="${url}" method="post">
+ <input type="hidden" name="came_from" value="${came_from}"/>
+ <input type="text" name="login" value="${login}"/>
+ <br/>
+ <input type="password" name="password" value="${password}"/>
+ <br/>
+ <input type="submit" name="form.submitted" value="Log In"/>
+ </form>
+</div>
+
+</body>
+</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..9178b5866
--- /dev/null
+++ b/docs/tutorials/wiki2/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="pyramid 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>Pyramid</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://pylonshq.com/pyramid">Pyramid</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>Pyramid</code> Documentation</h2>
+ <form method="get"
+ action="http://pylonshq.com/docs/pyramid/current/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>Pyramid</code> links</h2>
+ <ul>
+ <li><a
+ href="http://pylonshq.com/docs/pyramid/current/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#pylons">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/wiki2/src/authorization/tutorial/templates/static/default.css b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/wiki2/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/wiki2/src/authorization/tutorial/templates/static/images/img01.gif b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img02.gif b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img03.gif b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img04.gif b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/spacer.gif b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/style.css b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/style.css
new file mode 100644
index 000000000..cad87e0d4
--- /dev/null
+++ b/docs/tutorials/wiki2/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, 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/wiki2/src/authorization/tutorial/templates/static/templatelicense.txt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/wiki2/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/wiki2/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt
new file mode 100644
index 000000000..0c654250a
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.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>${page.name} - Pyramid 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/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
new file mode 100644
index 000000000..65330ce17
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
@@ -0,0 +1,139 @@
+import unittest
+
+from pyramid.configuration import Configurator
+from pyramid import testing
+
+def _initTestingDB():
+ from tutorial.models import DBSession
+ from tutorial.models import Base
+ from sqlalchemy import create_engine
+ engine = create_engine('sqlite://')
+ DBSession.configure(bind=engine)
+ Base.metadata.bind = engine
+ Base.metadata.create_all(engine)
+ return DBSession
+
+def _registerRoutes(config):
+ config.add_route('view_page', ':pagename')
+ config.add_route('edit_page', ':pagename/edit_page')
+ config.add_route('add_page', 'add_page/:pagename')
+
+class ViewWikiTests(unittest.TestCase):
+ def setUp(self):
+ self.config = Configurator()
+ self.config.begin()
+
+ def tearDown(self):
+ self.config.end()
+
+ def test_it(self):
+ from tutorial.views import view_wiki
+ self.config.add_route('view_page', ':pagename')
+ request = testing.DummyRequest()
+ response = view_wiki(request)
+ self.assertEqual(response.location, 'http://example.com/FrontPage')
+
+class ViewPageTests(unittest.TestCase):
+ def setUp(self):
+ self.session = _initTestingDB()
+ self.config = Configurator()
+ self.config.begin()
+
+ def tearDown(self):
+ self.session.remove()
+ self.config.end()
+
+ def _callFUT(self, request):
+ from tutorial.views import view_page
+ return view_page(request)
+
+ def test_it(self):
+ from tutorial.models import Page
+ request = testing.DummyRequest()
+ request.matchdict['pagename'] = 'IDoExist'
+ page = Page('IDoExist', 'Hello CruelWorld IDoExist')
+ self.session.add(page)
+ _registerRoutes(self.config)
+ info = self._callFUT(request)
+ self.assertEqual(info['page'], page)
+ self.assertEqual(
+ info['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(info['edit_url'],
+ 'http://example.com/IDoExist/edit_page')
+
+
+class AddPageTests(unittest.TestCase):
+ def setUp(self):
+ self.session = _initTestingDB()
+ self.config = Configurator()
+ self.config.begin()
+
+ def tearDown(self):
+ self.session.remove()
+ self.config.end()
+
+ def _callFUT(self, request):
+ from tutorial.views import add_page
+ return add_page(request)
+
+ def test_it_notsubmitted(self):
+ _registerRoutes(self.config)
+ request = testing.DummyRequest()
+ request.matchdict = {'pagename':'AnotherPage'}
+ info = self._callFUT(request)
+ self.assertEqual(info['page'].data,'')
+ self.assertEqual(info['save_url'],
+ 'http://example.com/add_page/AnotherPage')
+
+ def test_it_submitted(self):
+ from tutorial.models import Page
+ _registerRoutes(self.config)
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ request.matchdict = {'pagename':'AnotherPage'}
+ self._callFUT(request)
+ page = self.session.query(Page).filter_by(name='AnotherPage').one()
+ self.assertEqual(page.data, 'Hello yo!')
+
+class EditPageTests(unittest.TestCase):
+ def setUp(self):
+ self.session = _initTestingDB()
+ self.config = Configurator()
+ self.config.begin()
+
+ def tearDown(self):
+ self.session.remove()
+ self.config.end()
+
+ def _callFUT(self, request):
+ from tutorial.views import edit_page
+ return edit_page(request)
+
+ def test_it_notsubmitted(self):
+ from tutorial.models import Page
+ _registerRoutes(self.config)
+ request = testing.DummyRequest()
+ request.matchdict = {'pagename':'abc'}
+ page = Page('abc', 'hello')
+ self.session.add(page)
+ info = self._callFUT(request)
+ self.assertEqual(info['page'], page)
+ self.assertEqual(info['save_url'], 'http://example.com/abc/edit_page')
+
+ def test_it_submitted(self):
+ from tutorial.models import Page
+ _registerRoutes(self.config)
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ request.matchdict = {'pagename':'abc'}
+ page = Page('abc', 'hello')
+ self.session.add(page)
+ response = self._callFUT(request)
+ self.assertEqual(response.location, 'http://example.com/abc')
+ self.assertEqual(page.data, 'Hello yo!')
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
new file mode 100644
index 000000000..a7e7a57c7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
@@ -0,0 +1,71 @@
+import re
+
+from docutils.core import publish_parts
+
+from webob.exc import HTTPFound
+
+from pyramid.security import authenticated_userid
+from pyramid.url import route_url
+
+from tutorial.models import DBSession
+from tutorial.models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+def view_wiki(request):
+ return HTTPFound(location = route_url('view_page', request,
+ pagename='FrontPage'))
+
+def view_page(request):
+ pagename = request.matchdict['pagename']
+ session = DBSession()
+ page = session.query(Page).filter_by(name=pagename).one()
+
+ def check(match):
+ word = match.group(1)
+ exists = session.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = route_url('view_page', request, pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, word)
+ else:
+ add_url = route_url('add_page', request, pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, word)
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(check, content)
+ edit_url = route_url('edit_page', request, pagename=pagename)
+ logged_in = authenticated_userid(request)
+ return dict(page=page, content=content, edit_url=edit_url,
+ logged_in=logged_in)
+
+def add_page(request):
+ name = request.matchdict['pagename']
+ if 'form.submitted' in request.params:
+ session = DBSession()
+ body = request.params['body']
+ page = Page(name, body)
+ session.add(page)
+ return HTTPFound(location = route_url('view_page', request,
+ pagename=name))
+ save_url = route_url('add_page', request, pagename=name)
+ page = Page('', '')
+ logged_in = authenticated_userid(request)
+ return dict(page=page, save_url=save_url, logged_in=logged_in)
+
+def edit_page(request):
+ name = request.matchdict['pagename']
+ session = DBSession()
+ page = session.query(Page).filter_by(name=name).one()
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ session.add(page)
+ return HTTPFound(location = route_url('view_page', request,
+ pagename=name))
+
+ logged_in = authenticated_userid(request)
+ return dict(
+ page=page,
+ save_url = route_url('edit_page', request, pagename=name),
+ logged_in = logged_in,
+ )
diff --git a/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt b/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.cfg b/docs/tutorials/wiki2/src/basiclayout/setup.cfg
new file mode 100644
index 000000000..23b2ad983
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/setup.cfg
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package=tutorial
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = tutorial/locale
+domain = tutorial
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = tutorial/locale/tutorial.pot
+width = 80
+
+[init_catalog]
+domain = tutorial
+input_file = tutorial/locale/tutorial.pot
+output_dir = tutorial/locale
+
+[update_catalog]
+domain = tutorial
+input_file = tutorial/locale/tutorial.pot
+output_dir = tutorial/locale
+previous = true
diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py
new file mode 100644
index 000000000..aca548b32
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/setup.py
@@ -0,0 +1,45 @@
+import os
+import sys
+
+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 = [
+ 'pyramid',
+ 'SQLAlchemy',
+ 'transaction',
+ 'repoze.tm2',
+ 'zope.sqlalchemy',
+ ]
+
+if sys.version_info[:3] < (2,5,0):
+ requires.append('pysqlite')
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pylons",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi pylons pyramid bfg',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ test_suite='tutorial',
+ install_requires = requires,
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial.ini b/docs/tutorials/wiki2/src/basiclayout/tutorial.ini
new file mode 100644
index 000000000..73b5ed9a4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+debug = true
+
+[app:sqlalchemy]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+debug_templates = true
+default_locale_name = en
+db_string = sqlite:///%(here)s/tutorial.db
+db_echo = false
+
+[pipeline:main]
+pipeline =
+ egg:repoze.tm2#tm
+ sqlalchemy
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/configure.zcml b/docs/tutorials/wiki2/src/basiclayout/tutorial/configure.zcml
new file mode 100644
index 000000000..47adfbefb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/configure.zcml
@@ -0,0 +1,18 @@
+<configure xmlns="http://pylonshq.com/pyramid">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="pyramid.includes" />
+
+ <route
+ pattern=""
+ name="home"
+ view=".views.my_view"
+ view_renderer="templates/mytemplate.pt"
+ />
+
+ <static
+ name="static"
+ path="templates/static"
+ />
+
+</configure>
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py
new file mode 100644
index 000000000..ae71e7943
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py
@@ -0,0 +1,45 @@
+import transaction
+
+from sqlalchemy import create_engine
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import Unicode
+
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.ext.declarative import declarative_base
+
+from sqlalchemy.orm import scoped_session
+from sqlalchemy.orm import sessionmaker
+
+from zope.sqlalchemy import ZopeTransactionExtension
+
+DBSession = scoped_session(sessionmaker(
+ extension=ZopeTransactionExtension()))
+Base = declarative_base()
+
+class MyModel(Base):
+ __tablename__ = 'models'
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode(255), unique=True)
+ value = Column(Integer)
+
+ def __init__(self, name, value):
+ self.name = name
+ self.value = value
+
+def populate():
+ session = DBSession()
+ model = MyModel(name=u'root',value=55)
+ session.add(model)
+ session.flush()
+ transaction.commit()
+
+def initialize_sql(db_string, db_echo=False):
+ engine = create_engine(db_string, echo=db_echo)
+ DBSession.configure(bind=engine)
+ Base.metadata.bind = engine
+ Base.metadata.create_all(engine)
+ try:
+ populate()
+ except IntegrityError:
+ pass
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/run.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/run.py
new file mode 100644
index 000000000..7225987ee
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/run.py
@@ -0,0 +1,24 @@
+from pyramid.configuration import Configurator
+from paste.deploy.converters import asbool
+
+from tutorial.models import initialize_sql
+
+def app(global_config, **settings):
+ """ This function returns a WSGI application.
+
+ It is usually called by the PasteDeploy framework during
+ ``paster serve``.
+ """
+ zcml_file = settings.get('configure_zcml', 'configure.zcml')
+ db_string = settings.get('db_string')
+ if db_string is None:
+ raise ValueError(
+ "No 'db_string' value in application configuration.")
+ db_echo = settings.get('db_echo', 'false')
+ initialize_sql(db_string, asbool(db_echo))
+ config = Configurator(settings=settings)
+ config.begin()
+ config.load_zcml(zcml_file)
+ config.end()
+ return config.make_wsgi_app()
+
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..9178b5866
--- /dev/null
+++ b/docs/tutorials/wiki2/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="pyramid 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>Pyramid</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://pylonshq.com/pyramid">Pyramid</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>Pyramid</code> Documentation</h2>
+ <form method="get"
+ action="http://pylonshq.com/docs/pyramid/current/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>Pyramid</code> links</h2>
+ <ul>
+ <li><a
+ href="http://pylonshq.com/docs/pyramid/current/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#pylons">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/wiki2/src/basiclayout/tutorial/templates/static/default.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/wiki2/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/wiki2/src/basiclayout/tutorial/templates/static/images/img01.gif b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img02.gif b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img03.gif b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img04.gif b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/spacer.gif b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/templatelicense.txt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/wiki2/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/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
new file mode 100644
index 000000000..72f0c89d8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
@@ -0,0 +1,24 @@
+import unittest
+from pyramid.configuration import Configurator
+from pyramid import testing
+
+def _initTestingDB():
+ from tutorial.models import initialize_sql
+ session = initialize_sql('sqlite://')
+ return session
+
+class TestMyView(unittest.TestCase):
+ def setUp(self):
+ self.config = Configurator()
+ self.config.begin()
+ _initTestingDB()
+
+ def tearDown(self):
+ self.config.end()
+
+ def test_it(self):
+ from tutorial.views import my_view
+ request = testing.DummyRequest()
+ info = my_view(request)
+ self.assertEqual(info['root'].name, 'root')
+ self.assertEqual(info['project'], 'tutorial')
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py
new file mode 100644
index 000000000..e550e3257
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py
@@ -0,0 +1,7 @@
+from tutorial.models import DBSession
+from tutorial.models import MyModel
+
+def my_view(request):
+ dbsession = DBSession()
+ root = dbsession.query(MyModel).filter(MyModel.name==u'root').first()
+ return {'root':root, 'project':'tutorial'}
diff --git a/docs/tutorials/wiki2/src/models/CHANGES.txt b/docs/tutorials/wiki2/src/models/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/wiki2/src/models/setup.cfg b/docs/tutorials/wiki2/src/models/setup.cfg
new file mode 100644
index 000000000..23b2ad983
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/setup.cfg
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package=tutorial
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = tutorial/locale
+domain = tutorial
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = tutorial/locale/tutorial.pot
+width = 80
+
+[init_catalog]
+domain = tutorial
+input_file = tutorial/locale/tutorial.pot
+output_dir = tutorial/locale
+
+[update_catalog]
+domain = tutorial
+input_file = tutorial/locale/tutorial.pot
+output_dir = tutorial/locale
+previous = true
diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py
new file mode 100644
index 000000000..aca548b32
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/setup.py
@@ -0,0 +1,45 @@
+import os
+import sys
+
+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 = [
+ 'pyramid',
+ 'SQLAlchemy',
+ 'transaction',
+ 'repoze.tm2',
+ 'zope.sqlalchemy',
+ ]
+
+if sys.version_info[:3] < (2,5,0):
+ requires.append('pysqlite')
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pylons",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi pylons pyramid bfg',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ test_suite='tutorial',
+ install_requires = requires,
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/wiki2/src/models/tutorial.ini b/docs/tutorials/wiki2/src/models/tutorial.ini
new file mode 100644
index 000000000..73b5ed9a4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+debug = true
+
+[app:sqlalchemy]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+debug_templates = true
+default_locale_name = en
+db_string = sqlite:///%(here)s/tutorial.db
+db_echo = false
+
+[pipeline:main]
+pipeline =
+ egg:repoze.tm2#tm
+ sqlalchemy
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/wiki2/src/models/tutorial/configure.zcml b/docs/tutorials/wiki2/src/models/tutorial/configure.zcml
new file mode 100644
index 000000000..47adfbefb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/configure.zcml
@@ -0,0 +1,18 @@
+<configure xmlns="http://pylonshq.com/pyramid">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="pyramid.includes" />
+
+ <route
+ pattern=""
+ name="home"
+ view=".views.my_view"
+ view_renderer="templates/mytemplate.pt"
+ />
+
+ <static
+ name="static"
+ path="templates/static"
+ />
+
+</configure>
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models.py b/docs/tutorials/wiki2/src/models/tutorial/models.py
new file mode 100644
index 000000000..8c3f14915
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models.py
@@ -0,0 +1,43 @@
+import transaction
+
+from sqlalchemy import create_engine
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import Text
+
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.ext.declarative import declarative_base
+
+from sqlalchemy.orm import scoped_session
+from sqlalchemy.orm import sessionmaker
+
+from zope.sqlalchemy import ZopeTransactionExtension
+
+DBSession = scoped_session(sessionmaker(
+ extension=ZopeTransactionExtension()))
+Base = declarative_base()
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, unique=True)
+ data = Column(Text)
+
+ def __init__(self, name, data):
+ self.name = name
+ self.data = data
+
+def initialize_sql(db_string, echo=False):
+ engine = create_engine(db_string, echo=echo)
+ DBSession.configure(bind=engine)
+ Base.metadata.bind = engine
+ Base.metadata.create_all(engine)
+ try:
+ session = DBSession()
+ page = Page('FrontPage', 'initial data')
+ session.add(page)
+ transaction.commit()
+ except IntegrityError:
+ # already created
+ pass
diff --git a/docs/tutorials/wiki2/src/models/tutorial/run.py b/docs/tutorials/wiki2/src/models/tutorial/run.py
new file mode 100644
index 000000000..7225987ee
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/run.py
@@ -0,0 +1,24 @@
+from pyramid.configuration import Configurator
+from paste.deploy.converters import asbool
+
+from tutorial.models import initialize_sql
+
+def app(global_config, **settings):
+ """ This function returns a WSGI application.
+
+ It is usually called by the PasteDeploy framework during
+ ``paster serve``.
+ """
+ zcml_file = settings.get('configure_zcml', 'configure.zcml')
+ db_string = settings.get('db_string')
+ if db_string is None:
+ raise ValueError(
+ "No 'db_string' value in application configuration.")
+ db_echo = settings.get('db_echo', 'false')
+ initialize_sql(db_string, asbool(db_echo))
+ config = Configurator(settings=settings)
+ config.begin()
+ config.load_zcml(zcml_file)
+ config.end()
+ return config.make_wsgi_app()
+
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..9178b5866
--- /dev/null
+++ b/docs/tutorials/wiki2/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="pyramid 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>Pyramid</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://pylonshq.com/pyramid">Pyramid</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>Pyramid</code> Documentation</h2>
+ <form method="get"
+ action="http://pylonshq.com/docs/pyramid/current/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>Pyramid</code> links</h2>
+ <ul>
+ <li><a
+ href="http://pylonshq.com/docs/pyramid/current/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#pylons">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/wiki2/src/models/tutorial/templates/static/default.css b/docs/tutorials/wiki2/src/models/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/wiki2/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/wiki2/src/models/tutorial/templates/static/images/img01.gif b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img02.gif b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img03.gif b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img04.gif b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/spacer.gif b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/static/templatelicense.txt b/docs/tutorials/wiki2/src/models/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/wiki2/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/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py
new file mode 100644
index 000000000..72f0c89d8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py
@@ -0,0 +1,24 @@
+import unittest
+from pyramid.configuration import Configurator
+from pyramid import testing
+
+def _initTestingDB():
+ from tutorial.models import initialize_sql
+ session = initialize_sql('sqlite://')
+ return session
+
+class TestMyView(unittest.TestCase):
+ def setUp(self):
+ self.config = Configurator()
+ self.config.begin()
+ _initTestingDB()
+
+ def tearDown(self):
+ self.config.end()
+
+ def test_it(self):
+ from tutorial.views import my_view
+ request = testing.DummyRequest()
+ info = my_view(request)
+ self.assertEqual(info['root'].name, 'root')
+ self.assertEqual(info['project'], 'tutorial')
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views.py b/docs/tutorials/wiki2/src/models/tutorial/views.py
new file mode 100644
index 000000000..e550e3257
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/views.py
@@ -0,0 +1,7 @@
+from tutorial.models import DBSession
+from tutorial.models import MyModel
+
+def my_view(request):
+ dbsession = DBSession()
+ root = dbsession.query(MyModel).filter(MyModel.name==u'root').first()
+ return {'root':root, 'project':'tutorial'}
diff --git a/docs/tutorials/wiki2/src/views/CHANGES.txt b/docs/tutorials/wiki2/src/views/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt
new file mode 100644
index 000000000..d41f7f90f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/README.txt
@@ -0,0 +1,4 @@
+tutorial README
+
+
+
diff --git a/docs/tutorials/wiki2/src/views/setup.cfg b/docs/tutorials/wiki2/src/views/setup.cfg
new file mode 100644
index 000000000..23b2ad983
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/setup.cfg
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package=tutorial
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = tutorial/locale
+domain = tutorial
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = tutorial/locale/tutorial.pot
+width = 80
+
+[init_catalog]
+domain = tutorial
+input_file = tutorial/locale/tutorial.pot
+output_dir = tutorial/locale
+
+[update_catalog]
+domain = tutorial
+input_file = tutorial/locale/tutorial.pot
+output_dir = tutorial/locale
+previous = true
diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py
new file mode 100644
index 000000000..eeac5c397
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/setup.py
@@ -0,0 +1,46 @@
+import os
+import sys
+
+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 = [
+ 'pyramid',
+ 'SQLAlchemy',
+ 'transaction',
+ 'repoze.tm2',
+ 'zope.sqlalchemy',
+ 'docutils'
+ ]
+
+if sys.version_info[:3] < (2,5,0):
+ requires.append('pysqlite')
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pylons",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi pylons pyramid bfg',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ test_suite='tutorial',
+ install_requires = requires,
+ entry_points = """\
+ [paste.app_factory]
+ app = tutorial.run:app
+ """
+ )
+
diff --git a/docs/tutorials/wiki2/src/views/tutorial.ini b/docs/tutorials/wiki2/src/views/tutorial.ini
new file mode 100644
index 000000000..85f131c2e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+debug = true
+
+[app:sqlalchemy]
+use = egg:tutorial#app
+reload_templates = true
+debug_authorization = false
+debug_notfound = false
+debug_templates = true
+default_locale_name = en
+db_string = sqlite:///%(here)s/tutorial.db
+db_echo = false
+
+[pipeline:main]
+pipeline =
+ egg:Paste#evalerror
+ egg:repoze.tm2#tm
+ sqlalchemy
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
new file mode 100644
index 000000000..cbdfd3ac6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
@@ -0,0 +1,2 @@
+# A package
+
diff --git a/docs/tutorials/wiki2/src/views/tutorial/configure.zcml b/docs/tutorials/wiki2/src/views/tutorial/configure.zcml
new file mode 100644
index 000000000..3a2e13a7a
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/configure.zcml
@@ -0,0 +1,38 @@
+<configure xmlns="http://pylonshq.com/pyramid">
+
+ <!-- this must be included for the view declarations to work -->
+ <include package="pyramid.includes" />
+
+ <static
+ path="templates/static"
+ name="static"
+ />
+
+ <route
+ pattern=""
+ name="view_wiki"
+ view=".views.view_wiki"
+ />
+
+ <route
+ pattern=":pagename"
+ name="view_page"
+ view=".views.view_page"
+ view_renderer="templates/view.pt"
+ />
+
+ <route
+ pattern="add_page/:pagename"
+ name="add_page"
+ view=".views.add_page"
+ view_renderer="templates/edit.pt"
+ />
+
+ <route
+ pattern=":pagename/edit_page"
+ name="edit_page"
+ view=".views.edit_page"
+ view_renderer="templates/edit.pt"
+ />
+
+</configure>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models.py b/docs/tutorials/wiki2/src/views/tutorial/models.py
new file mode 100644
index 000000000..8c3f14915
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models.py
@@ -0,0 +1,43 @@
+import transaction
+
+from sqlalchemy import create_engine
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import Text
+
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.ext.declarative import declarative_base
+
+from sqlalchemy.orm import scoped_session
+from sqlalchemy.orm import sessionmaker
+
+from zope.sqlalchemy import ZopeTransactionExtension
+
+DBSession = scoped_session(sessionmaker(
+ extension=ZopeTransactionExtension()))
+Base = declarative_base()
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, unique=True)
+ data = Column(Text)
+
+ def __init__(self, name, data):
+ self.name = name
+ self.data = data
+
+def initialize_sql(db_string, echo=False):
+ engine = create_engine(db_string, echo=echo)
+ DBSession.configure(bind=engine)
+ Base.metadata.bind = engine
+ Base.metadata.create_all(engine)
+ try:
+ session = DBSession()
+ page = Page('FrontPage', 'initial data')
+ session.add(page)
+ transaction.commit()
+ except IntegrityError:
+ # already created
+ pass
diff --git a/docs/tutorials/wiki2/src/views/tutorial/run.py b/docs/tutorials/wiki2/src/views/tutorial/run.py
new file mode 100644
index 000000000..7225987ee
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/run.py
@@ -0,0 +1,24 @@
+from pyramid.configuration import Configurator
+from paste.deploy.converters import asbool
+
+from tutorial.models import initialize_sql
+
+def app(global_config, **settings):
+ """ This function returns a WSGI application.
+
+ It is usually called by the PasteDeploy framework during
+ ``paster serve``.
+ """
+ zcml_file = settings.get('configure_zcml', 'configure.zcml')
+ db_string = settings.get('db_string')
+ if db_string is None:
+ raise ValueError(
+ "No 'db_string' value in application configuration.")
+ db_echo = settings.get('db_echo', 'false')
+ initialize_sql(db_string, asbool(db_echo))
+ config = Configurator(settings=settings)
+ config.begin()
+ config.load_zcml(zcml_file)
+ config.end()
+ return config.make_wsgi_app()
+
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt
new file mode 100644
index 000000000..047a64eb3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt
@@ -0,0 +1,32 @@
+<!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>Pyramid 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/wiki2/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..9178b5866
--- /dev/null
+++ b/docs/tutorials/wiki2/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="pyramid 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>Pyramid</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://pylonshq.com/pyramid">Pyramid</a> web
+ application framework.</h1>
+ </div>
+ </div>
+ <!-- end content -->
+ <!-- start sidebar -->
+ <div id="sidebar">
+ <ul>
+ <li id="search">
+ <h2>Search<br/> <code>Pyramid</code> Documentation</h2>
+ <form method="get"
+ action="http://pylonshq.com/docs/pyramid/current/searchresults">
+ <fieldset>
+ <input type="text" id="q" name="text" value="" />
+ <input type="submit" id="x" value="Search" />
+ </fieldset>
+ </form>
+ </li>
+ <li>
+ <h2><code>Pyramid</code> links</h2>
+ <ul>
+ <li><a
+ href="http://pylonshq.com/docs/pyramid/current/#narrative-documentation">Narrative
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#api-documentation">API
+ Documentation</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#tutorials">Tutorials</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#change-history">Change
+ History</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#sample-applications">Sample
+ Applications</a>
+ </li>
+ <li>
+ <a
+ href="http://pylonshq.com/docs/pyramid/current/#support-and-development">Support
+ and Development</a>
+ </li>
+ <li>
+ <a
+ href="irc://irc.freenode.net#pylons">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/wiki2/src/views/tutorial/templates/static/default.css b/docs/tutorials/wiki2/src/views/tutorial/templates/static/default.css
new file mode 100644
index 000000000..41b3debde
--- /dev/null
+++ b/docs/tutorials/wiki2/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/wiki2/src/views/tutorial/templates/static/images/img01.gif b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img01.gif
new file mode 100644
index 000000000..5f082bd99
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img01.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img02.gif b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img02.gif
new file mode 100644
index 000000000..45a3ae976
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img02.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img03.gif b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img03.gif
new file mode 100644
index 000000000..d92ea38f9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img03.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img04.gif b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img04.gif
new file mode 100644
index 000000000..950c4af9d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/img04.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/spacer.gif b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/spacer.gif
new file mode 100644
index 000000000..5bfd67a2d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/static/images/spacer.gif
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/static/style.css b/docs/tutorials/wiki2/src/views/tutorial/templates/static/style.css
new file mode 100644
index 000000000..cad87e0d4
--- /dev/null
+++ b/docs/tutorials/wiki2/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, 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/wiki2/src/views/tutorial/templates/static/templatelicense.txt b/docs/tutorials/wiki2/src/views/tutorial/templates/static/templatelicense.txt
new file mode 100644
index 000000000..ccb6b06ab
--- /dev/null
+++ b/docs/tutorials/wiki2/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/wiki2/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt
new file mode 100644
index 000000000..86fcc914e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt
@@ -0,0 +1,28 @@
+<!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} - Pyramid 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/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py
new file mode 100644
index 000000000..40336fca4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py
@@ -0,0 +1,140 @@
+import unittest
+
+from pyramid.configuration import Configurator
+from pyramid import testing
+
+def _initTestingDB():
+ from tutorial.models import DBSession
+ from tutorial.models import Base
+ from sqlalchemy import create_engine
+ engine = create_engine('sqlite://')
+ DBSession.configure(bind=engine)
+ Base.metadata.bind = engine
+ Base.metadata.create_all(engine)
+ return DBSession
+
+def _registerRoutes(config):
+ config.add_route('view_page', ':pagename')
+ config.add_route('edit_page', ':pagename/edit_page')
+ config.add_route('add_page', 'add_page/:pagename')
+
+class ViewWikiTests(unittest.TestCase):
+ def setUp(self):
+ self.config = Configurator()
+ self.config.begin()
+
+ def tearDown(self):
+ self.config.end()
+
+ def test_it(self):
+ from tutorial.views import view_wiki
+ self.config.add_route('view_page', ':pagename')
+ request = testing.DummyRequest()
+ response = view_wiki(request)
+ self.assertEqual(response.location, 'http://example.com/FrontPage')
+
+class ViewPageTests(unittest.TestCase):
+ def setUp(self):
+ self.session = _initTestingDB()
+ self.config = Configurator()
+ self.config.begin()
+
+ def tearDown(self):
+ self.session.remove()
+ self.config.end()
+
+ def _callFUT(self, request):
+ from tutorial.views import view_page
+ return view_page(request)
+
+ def test_it(self):
+ from tutorial.models import Page
+ request = testing.DummyRequest()
+ request.matchdict['pagename'] = 'IDoExist'
+ page = Page('IDoExist', 'Hello CruelWorld IDoExist')
+ self.session.add(page)
+ _registerRoutes(self.config)
+ info = self._callFUT(request)
+ self.assertEqual(info['page'], page)
+ self.assertEqual(
+ info['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(info['edit_url'],
+ 'http://example.com/IDoExist/edit_page')
+
+
+class AddPageTests(unittest.TestCase):
+ def setUp(self):
+ self.session = _initTestingDB()
+ self.config = Configurator()
+ self.config.begin()
+
+ def tearDown(self):
+ self.session.remove()
+ self.config.end()
+
+ def _callFUT(self, request):
+ from tutorial.views import add_page
+ return add_page(request)
+
+ def test_it_notsubmitted(self):
+ _registerRoutes(self.config)
+ request = testing.DummyRequest()
+ request.matchdict = {'pagename':'AnotherPage'}
+ info = self._callFUT(request)
+ self.assertEqual(info['page'].data,'')
+ self.assertEqual(info['save_url'],
+ 'http://example.com/add_page/AnotherPage')
+
+ def test_it_submitted(self):
+ from tutorial.models import Page
+ _registerRoutes(self.config)
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ request.matchdict = {'pagename':'AnotherPage'}
+ self._callFUT(request)
+ page = self.session.query(Page).filter_by(name='AnotherPage').one()
+ self.assertEqual(page.data, 'Hello yo!')
+
+class EditPageTests(unittest.TestCase):
+ def setUp(self):
+ self.session = _initTestingDB()
+ self.config = Configurator()
+ self.config.begin()
+
+ def tearDown(self):
+ self.session.remove()
+ self.config.end()
+
+ def _callFUT(self, request):
+ from tutorial.views import edit_page
+ return edit_page(request)
+
+ def test_it_notsubmitted(self):
+ from tutorial.models import Page
+ _registerRoutes(self.config)
+ request = testing.DummyRequest()
+ request.matchdict = {'pagename':'abc'}
+ page = Page('abc', 'hello')
+ self.session.add(page)
+ info = self._callFUT(request)
+ self.assertEqual(info['page'], page)
+ self.assertEqual(info['save_url'],
+ 'http://example.com/abc/edit_page')
+
+ def test_it_submitted(self):
+ from tutorial.models import Page
+ _registerRoutes(self.config)
+ request = testing.DummyRequest({'form.submitted':True,
+ 'body':'Hello yo!'})
+ request.matchdict = {'pagename':'abc'}
+ page = Page('abc', 'hello')
+ self.session.add(page)
+ response = self._callFUT(request)
+ self.assertEqual(response.location, 'http://example.com/abc')
+ self.assertEqual(page.data, 'Hello yo!')
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py
new file mode 100644
index 000000000..c0d793d38
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/views.py
@@ -0,0 +1,65 @@
+import re
+
+from docutils.core import publish_parts
+
+from webob.exc import HTTPFound
+
+from pyramid.url import route_url
+
+from tutorial.models import DBSession
+from tutorial.models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+def view_wiki(request):
+ return HTTPFound(location = route_url('view_page', request,
+ pagename='FrontPage'))
+
+def view_page(request):
+ matchdict = request.matchdict
+ session = DBSession()
+ page = session.query(Page).filter_by(name=matchdict['pagename']).one()
+
+ def check(match):
+ word = match.group(1)
+ exists = session.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = route_url('view_page', request, pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, word)
+ else:
+ add_url = route_url('add_page', request, pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, word)
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(check, content)
+ edit_url = route_url('edit_page', request,
+ pagename=matchdict['pagename'])
+ return dict(page=page, content=content, edit_url=edit_url)
+
+def add_page(request):
+ name = request.matchdict['pagename']
+ if 'form.submitted' in request.params:
+ session = DBSession()
+ body = request.params['body']
+ page = Page(name, body)
+ session.add(page)
+ return HTTPFound(location = route_url('view_page', request,
+ pagename=name))
+ save_url = route_url('add_page', request, pagename=name)
+ page = Page('', '')
+ return dict(page=page, save_url=save_url)
+
+def edit_page(request):
+ name = request.matchdict['pagename']
+ session = DBSession()
+ page = session.query(Page).filter_by(name=name).one()
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ session.add(page)
+ return HTTPFound(location = route_url('view_page', request,
+ pagename=name))
+ return dict(
+ page=page,
+ save_url = route_url('edit_page', request, pagename=name),
+ )