summaryrefslogtreecommitdiff
path: root/docs/tutorials
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2016-03-21 23:34:26 -0500
committerMichael Merickel <michael@merickel.org>2016-03-21 23:34:26 -0500
commit5238bb64abfebc085ca95df517535f61e27b7fc2 (patch)
treec4eb14e487f561d4c12824b0b03656e24efccfd1 /docs/tutorials
parent0b0f7e1a6d411bcc2af615da8e9dec7ea7519152 (diff)
parent530bad618c33d65ad071b0280ce56eb265195805 (diff)
downloadpyramid-5238bb64abfebc085ca95df517535f61e27b7fc2.tar.gz
pyramid-5238bb64abfebc085ca95df517535f61e27b7fc2.tar.bz2
pyramid-5238bb64abfebc085ca95df517535f61e27b7fc2.zip
Merge branch 'master' into feature/configurable-view-deriver
Diffstat (limited to 'docs/tutorials')
-rw-r--r--docs/tutorials/wiki/authorization.rst4
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/templates/view.pt2
-rw-r--r--docs/tutorials/wiki2/authentication.rst312
-rw-r--r--docs/tutorials/wiki2/authorization.rst501
-rw-r--r--docs/tutorials/wiki2/background.rst4
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst396
-rw-r--r--docs/tutorials/wiki2/definingmodels.rst279
-rw-r--r--docs/tutorials/wiki2/definingviews.rst492
-rw-r--r--docs/tutorials/wiki2/design.rst254
-rw-r--r--docs/tutorials/wiki2/distributing.rst12
-rw-r--r--docs/tutorials/wiki2/index.rst13
-rw-r--r--docs/tutorials/wiki2/installation.rst339
-rw-r--r--docs/tutorials/wiki2/src/authentication/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki2/src/authentication/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/authentication/README.txt14
-rw-r--r--docs/tutorials/wiki2/src/authentication/development.ini73
-rw-r--r--docs/tutorials/wiki2/src/authentication/production.ini62
-rw-r--r--docs/tutorials/wiki2/src/authentication/setup.py54
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/__init__.py13
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/routes.py8
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py1
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py57
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/security.py27
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.pngbin0 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/views/tutorial/templates/view.pt)37
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/tests.py65
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/default.py79
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/authorization/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/authorization/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/authorization/production.ini18
-rw-r--r--docs/tutorials/wiki2/src/authorization/setup.py10
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/__init__.py34
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models.py37
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/routes.py56
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py44
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/security.py45
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css2
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt72
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt)40
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt74
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt66
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/tests.py175
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views.py124
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/default.py64
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/development.ini4
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/production.ini4
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/setup.py7
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py15
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models.py27
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py73
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py3
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py30
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt)18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja28
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt66
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py66
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py (renamed from docs/tutorials/wiki2/src/basiclayout/tutorial/views.py)18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/models/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/models/development.ini4
-rw-r--r--docs/tutorials/wiki2/src/models/production.ini16
-rw-r--r--docs/tutorials/wiki2/src/models/setup.py8
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/__init__.py15
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models.py25
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/routes.py3
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py44
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css2
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt)18
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja28
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/tests.py66
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/default.py (renamed from docs/tutorials/wiki2/src/models/tutorial/views.py)18
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/tests/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/tests/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/tests/production.ini18
-rw-r--r--docs/tutorials/wiki2/src/tests/setup.py11
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/__init__.py34
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models.py37
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/routes.py56
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py44
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/security.py45
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css2
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt74
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt)37
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt54
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests.py235
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py122
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py168
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views.py123
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/default.py64
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/views/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/views/development.ini4
-rw-r--r--docs/tutorials/wiki2/src/views/production.ini16
-rw-r--r--docs/tutorials/wiki2/src/views/setup.py10
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/__init__.py18
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models.py25
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/routes.py6
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py44
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt)33
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt66
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/tests.py175
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views.py72
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/default.py73
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/tests.rst105
162 files changed, 4762 insertions, 2906 deletions
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index b0a8c155d..c6f551b42 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -248,7 +248,7 @@ Return a ``logged_in`` flag to the renderer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Open ``tutorial/tutorial/views.py`` again. Add a ``logged_in`` parameter to
-the return value of ``view_page()``, ``edit_page()``, and ``add_page()`` as
+the return value of ``view_page()``, ``add_page()``, and ``edit_page()`` as
follows:
.. literalinclude:: src/authorization/tutorial/views.py
@@ -262,7 +262,7 @@ follows:
:language: python
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 75-77
+ :lines: 78-80
:emphasize-lines: 2-3
:language: python
diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
index e7b0dc23e..93580658b 100644
--- a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
@@ -8,7 +8,7 @@
<meta name="author" content="Pylons Project">
<link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
- <title>${page.name} - Pyramid tutorial wiki (based on
+ <title>${page.__name__} - Pyramid tutorial wiki (based on
TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
new file mode 100644
index 000000000..5447db861
--- /dev/null
+++ b/docs/tutorials/wiki2/authentication.rst
@@ -0,0 +1,312 @@
+.. _wiki2_adding_authentication:
+
+=====================
+Adding authentication
+=====================
+
+:app:`Pyramid` provides facilities for :term:`authentication` and
+:term:`authorization`. In this section we'll focus solely on the authentication
+APIs to add login and logout functionality to our wiki.
+
+We will implement authentication with the following steps:
+
+* Add an :term:`authentication policy` and a ``request.user`` computed property
+ (``security.py``).
+* Add routes for ``/login`` and ``/logout`` (``routes.py``).
+* Add login and logout views (``views/auth.py``).
+* Add a login template (``login.jinja2``).
+* Add "Login" and "Logout" links to every page based on the user's
+ authenticated state (``layout.jinja2``).
+* Make the existing views verify user state (``views/default.py``).
+* Redirect to ``/login`` when a user is denied access to any of the views that
+ require permission, instead of a default "403 Forbidden" page
+ (``views/auth.py``).
+
+
+Authenticating requests
+-----------------------
+
+The core of :app:`Pyramid` authentication is an :term:`authentication policy`
+which is used to identify authentication information from a ``request``,
+as well as handling the low-level login and logout operations required to
+track users across requests (via cookies, headers, or whatever else you can
+imagine).
+
+
+Add the authentication policy
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create a new file ``tutorial/security.py`` with the following content:
+
+.. literalinclude:: src/authentication/tutorial/security.py
+ :linenos:
+ :language: python
+
+Here we've defined:
+
+* A new authentication policy named ``MyAuthenticationPolicy``, which is
+ subclassed from Pyramid's
+ :class:`pyramid.authentication.AuthTktAuthenticationPolicy`, which tracks the
+ :term:`userid` using a signed cookie (lines 7-11).
+* A ``get_user`` function, which can convert the ``unauthenticated_userid``
+ from the policy into a ``User`` object from our database (lines 13-17).
+* The ``get_user`` is registered on the request as ``request.user`` to be used
+ throughout our application as the authenticated ``User`` object for the
+ logged-in user (line 27).
+
+The logic in this file is a little bit interesting, so we'll go into detail
+about what's happening here:
+
+First, the default authentication policies all provide a method named
+``unauthenticated_userid`` which is responsible for the low-level parsing
+of the information in the request (cookies, headers, etc.). If a ``userid``
+is found, then it is returned from this method. This is named
+``unauthenticated_userid`` because, at the lowest level, it knows the value of
+the userid in the cookie, but it doesn't know if it's actually a user in our
+system (remember, anything the user sends to our app is untrusted).
+
+Second, our application should only care about ``authenticated_userid`` and
+``request.user``, which have gone through our application-specific process of
+validating that the user is logged in.
+
+In order to provide an ``authenticated_userid`` we need a verification step.
+That can happen anywhere, so we've elected to do it inside of the cached
+``request.user`` computed property. This is a convenience that makes
+``request.user`` the source of truth in our system. It is either ``None`` or
+a ``User`` object from our database. This is why the ``get_user`` function
+uses the ``unauthenticated_userid`` to check the database.
+
+
+Configure the app
+~~~~~~~~~~~~~~~~~
+
+Since we've added a new ``tutorial/security.py`` module, we need to include it.
+Open the file ``tutorial/__init__.py`` and edit the following lines:
+
+.. literalinclude:: src/authentication/tutorial/__init__.py
+ :linenos:
+ :emphasize-lines: 11
+ :language: python
+
+Our authentication policy is expecting a new setting, ``auth.secret``. Open
+the file ``development.ini`` and add the highlighted line below:
+
+.. literalinclude:: src/authentication/development.ini
+ :lines: 18-20
+ :emphasize-lines: 3
+ :lineno-match:
+ :language: ini
+
+Finally, best practices tell us to use a different secret for production, so
+open ``production.ini`` and add a different secret:
+
+.. literalinclude:: src/authentication/production.ini
+ :lines: 15-17
+ :emphasize-lines: 3
+ :lineno-match:
+ :language: ini
+
+
+Add permission checks
+~~~~~~~~~~~~~~~~~~~~~
+
+:app:`Pyramid` has full support for declarative authorization, which we'll
+cover in the next chapter. However, many people looking to get their feet wet
+are just interested in authentication with some basic form of home-grown
+authorization. We'll show below how to accomplish the simple security goals of
+our wiki, now that we can track the logged-in state of users.
+
+Remember our goals:
+
+* Allow only ``editor`` and ``basic`` logged-in users to create new pages.
+* Only allow ``editor`` users and the page creator (possibly a ``basic`` user)
+ to edit pages.
+
+Open the file ``tutorial/views/default.py`` and fix the following imports:
+
+.. literalinclude:: src/authentication/tutorial/views/default.py
+ :lines: 5-13
+ :lineno-match:
+ :emphasize-lines: 2,9
+ :language: python
+
+Change the two highlighted lines.
+
+In the same file, now edit the ``edit_page`` view function:
+
+.. literalinclude:: src/authentication/tutorial/views/default.py
+ :lines: 45-60
+ :lineno-match:
+ :emphasize-lines: 5-7
+ :language: python
+
+Only the highlighted lines need to be changed.
+
+If the user either is not logged in or the user is not the page's creator
+*and* not an ``editor``, then we raise ``HTTPForbidden``.
+
+In the same file, now edit the ``add_page`` view function:
+
+.. literalinclude:: src/authentication/tutorial/views/default.py
+ :lines: 62-76
+ :lineno-match:
+ :emphasize-lines: 3-5,13
+ :language: python
+
+Only the highlighted lines need to be changed.
+
+If the user either is not logged in or is not in the ``basic`` or ``editor``
+roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden"
+response to the user. However, we will hook this later to redirect to the login
+page. Also, now that we have ``request.user``, we no longer have to hard-code
+the creator as the ``editor`` user, so we can finally drop that hack.
+
+These simple checks should protect our views.
+
+
+Login, logout
+-------------
+
+Now that we've got the ability to detect logged-in users, we need to add the
+``/login`` and ``/logout`` views so that they can actually login and logout!
+
+
+Add routes for ``/login`` and ``/logout``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Go back to ``tutorial/routes.py`` and add these two routes as highlighted:
+
+.. literalinclude:: src/authentication/tutorial/routes.py
+ :lines: 3-6
+ :lineno-match:
+ :emphasize-lines: 2-3
+ :language: python
+
+.. note:: The preceding lines must be added *before* the following
+ ``view_page`` route definition:
+
+ .. literalinclude:: src/authentication/tutorial/routes.py
+ :lines: 6
+ :lineno-match:
+ :language: python
+
+ This is because ``view_page``'s route definition uses a catch-all
+ "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`),
+ which will catch any route that was not already caught by any route
+ registered before it. Hence, for ``login`` and ``logout`` views to
+ have the opportunity of being matched (or "caught"), they must be above
+ ``/{pagename}``.
+
+
+Add login, logout, and forbidden views
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create a new file ``tutorial/views/auth.py``, and add the following code to it:
+
+.. literalinclude:: src/authentication/tutorial/views/auth.py
+ :linenos:
+ :language: python
+
+This code adds three new views to the application:
+
+- The ``login`` view renders a login form and processes the post from the
+ login form, checking credentials against our ``users`` table in the database.
+
+ The check is done by first finding a ``User`` record in the database, then
+ using our ``user.check_password`` method to compare the hashed passwords.
+
+ If the credentials are valid, then we use our authentication policy to store
+ the user's id in the response using :meth:`pyramid.security.remember`.
+
+ Finally, the user is redirected back to either the page which they were
+ trying to access (``next``) or the front page as a fallback. This parameter
+ is used by our forbidden view, as explained below, to finish the login
+ workflow.
+
+- The ``logout`` view handles requests to ``/logout`` by clearing the
+ credentials using :meth:`pyramid.security.forget`, then redirecting them to
+ the front page.
+
+- The ``forbidden_view`` is registered using the
+ :class:`pyramid.view.forbidden_view_config` decorator. This is a special
+ :term:`exception view`, which is invoked when a
+ :class:`pyramid.httpexceptions.HTTPForbidden` exception is raised.
+
+ This view will handle a forbidden error by redirecting the user to
+ ``/login``. As a convenience, it also sets the ``next=`` query string to the
+ current URL (the one that is forbidding access). This way, if the user
+ successfully logs in, they will be sent back to the page which they had been
+ trying to access.
+
+
+Add the ``login.jinja2`` template
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create ``tutorial/templates/login.jinja2`` with the following content:
+
+.. literalinclude:: src/authentication/tutorial/templates/login.jinja2
+ :language: html
+
+The above template is referenced in the login view that we just added in
+``tutorial/views/auth.py``.
+
+
+Add "Login" and "Logout" links
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Open ``tutorial/templates/layout.jinja2`` and add the following code as
+indicated by the highlighted lines.
+
+.. literalinclude:: src/authentication/tutorial/templates/layout.jinja2
+ :lines: 35-46
+ :lineno-match:
+ :emphasize-lines: 2-10
+ :language: html
+
+The ``request.user`` will be ``None`` if the user is not authenticated, or a
+``tutorial.models.User`` object if the user is authenticated. This check will
+make the logout link shown only when the user is logged in, and conversely the
+login link is only shown when the user is logged out.
+
+
+Viewing the application in a browser
+------------------------------------
+
+We can finally examine our application in a browser (See
+:ref:`wiki2-start-the-application`). Launch a browser and visit each of the
+following URLs, checking that the result is as expected:
+
+- http://localhost:6543/ invokes the ``view_wiki`` view. This always
+ redirects to the ``view_page`` view of the ``FrontPage`` page object. It
+ is executable by any user.
+
+- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
+ ``FrontPage`` page object. There is a "Login" link in the upper right corner
+ while the user is not authenticated, else it is a "Logout" link when the user
+ is authenticated.
+
+- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for
+ the ``FrontPage`` page object. It is executable by only the ``editor`` user.
+ If a different user (or the anonymous user) invokes it, then a login form
+ will be displayed. Supplying the credentials with the username ``editor`` and
+ password ``editor`` will display the edit page form.
+
+- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for
+ a page. If the page already exists, then it redirects the user to the
+ ``edit_page`` view for the page object. It is executable by either the
+ ``editor`` or ``basic`` user. If a different user (or the anonymous user)
+ invokes it, then a login form will be displayed. Supplying the credentials
+ with either the username ``editor`` and password ``editor``, or username
+ ``basic`` and password ``basic``, will display the edit page form.
+
+- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view
+ for an existing page, or generates an error if the page does not exist. It is
+ editable by the ``basic`` user if the page was created by that user in the
+ previous step. If, instead, the page was created by the ``editor`` user, then
+ the login page should be shown for the ``basic`` user.
+
+- After logging 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, redirected
+ back to the front page, and a "Login" link is shown in the upper right hand
+ corner.
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index 1d810b05b..962e2859c 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -4,375 +4,221 @@
Adding authorization
====================
-:app:`Pyramid` provides facilities for :term:`authentication` and
-:term:`authorization`. We'll make use of both features to provide security
-to our application. Our application currently allows anyone with access to
-the server to view, edit, and add pages to our wiki. We'll change that to
-allow only people who are members of a *group* named ``group:editors`` to add
-and edit wiki pages but we'll continue allowing anyone with access to the
-server to view pages.
-
-We will also add a login page and a logout link on all the pages. The login
-page will be shown when a user is denied access to any of the views that
-require permission, instead of a default "403 Forbidden" page.
-
-We will implement the access control with the following steps:
-
-* Add users and groups (``security.py``, a new module).
-* Add an :term:`ACL` (``models.py`` and ``__init__.py``).
-* Add an :term:`authentication policy` and an :term:`authorization policy`
- (``__init__.py``).
-* Add :term:`permission` declarations to the ``edit_page`` and ``add_page``
- views (``views.py``).
-
-Then we will add the login and logout feature:
-
-* Add routes for /login and /logout (``__init__.py``).
-* Add ``login`` and ``logout`` views (``views.py``).
-* Add a login template (``login.pt``).
-* Make the existing views return a ``logged_in`` flag to the renderer
- (``views.py``).
-* Add a "Logout" link to be shown when logged in and viewing or editing a page
- (``view.pt``, ``edit.pt``).
-
-
-Access control
---------------
-
-Add users and groups
-~~~~~~~~~~~~~~~~~~~~
-
-Create a new ``tutorial/tutorial/security.py`` module with the
-following content:
+In the last chapter we built :term:`authentication` into our wiki2. We also
+went one step further and used the ``request.user`` object to perform some
+explicit :term:`authorization` checks. This is fine for a lot of applications,
+but :app:`Pyramid` provides some facilities for cleaning this up and decoupling
+the constraints from the view function itself.
+
+We will implement access control with the following steps:
+
+* Update the :term:`authentication policy` to break down the :term:`userid`
+ into a list of :term:`principals <principal>` (``security.py``).
+* Define an :term:`authorization policy` for mapping users, resources and
+ permissions (``security.py``).
+* Add new :term:`resource` definitions that will be used as the :term:`context`
+ for the wiki pages (``routes.py``).
+* Add an :term:`ACL` to each resource (``routes.py``).
+* Replace the inline checks on the views with :term:`permission` declarations
+ (``views/default.py``).
+
+
+Add user principals
+-------------------
+
+A :term:`principal` is a level of abstraction on top of the raw :term:`userid`
+that describes the user in terms of its capabilities, roles, or other
+identifiers that are easier to generalize. The permissions are then written
+against the principals without focusing on the exact user involved.
+
+:app:`Pyramid` defines two builtin principals used in every application:
+:attr:`pyramid.security.Everyone` and :attr:`pyramid.security.Authenticated`.
+On top of these we have already mentioned the required principals for this
+application in the original design. The user has two possible roles: ``editor``
+or ``basic``. These will be prefixed by the string ``role:`` to avoid clashing
+with any other types of principals.
+
+Open the file ``tutorial/security.py`` and edit it as follows:
.. literalinclude:: src/authorization/tutorial/security.py
:linenos:
+ :emphasize-lines: 3-6,17-24
:language: python
-The ``groupfinder`` function accepts a userid and a request and
-returns one of these values:
+Only the highlighted lines need to be added.
-- If the userid exists in the system, it 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, it will return ``None``.
+Note that the role comes from the ``User`` object. We also add the ``user.id``
+as a principal for when we want to allow that exact user to edit pages which
+they have created.
-For example, ``groupfinder('editor', request )`` returns ``['group:editor']``,
-``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin',
-request)`` returns ``None``. We will use ``groupfinder()`` as an
-:term:`authentication policy` "callback" that will provide the
-:term:`principal` or principals for a user.
-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.
+Add the authorization policy
+----------------------------
-Add an ACL
-~~~~~~~~~~
+We already added the :term:`authorization policy` in the previous chapter
+because :app:`Pyramid` requires one when adding an
+:term:`authentication policy`. However, it was not used anywhere, so we'll
+mention it now.
-Open ``tutorial/tutorial/models.py`` and add the following import
-statement at the head:
+In the file ``tutorial/security.py``, notice the following lines:
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 1-4
- :linenos:
+.. literalinclude:: src/authorization/tutorial/security.py
+ :lines: 38-40
+ :lineno-match:
+ :emphasize-lines: 2
:language: python
-Add the following class definition at the end:
+We're using the :class:`pyramid.authorization.ACLAuthorizationPolicy`, which
+will suffice for most applications. It uses the :term:`context` to define the
+mapping between a :term:`principal` and :term:`permission` for the current
+request via the ``__acl__``.
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 33-37
- :linenos:
- :lineno-start: 33
- :language: python
-We import :data:`~pyramid.security.Allow`, an action that means that
-permission is allowed, and :data:`~pyramid.security.Everyone`, a special
-:term:`principal` that is associated to all requests. Both are used in the
-:term:`ACE` entries that make up the ACL.
+Add resources and ACLs
+----------------------
-The ACL is a list that needs to be named `__acl__` and be an attribute of a
-class. We define an :term:`ACL` with two :term:`ACE` entries: the first entry
-allows any user the `view` permission. The second entry allows the
-``group:editors`` principal the `edit` permission.
+Resources are the hidden gem of :app:`Pyramid`. You've made it!
-The ``RootFactory`` class that contains the ACL is a :term:`root factory`. We
-need to associate it to our :app:`Pyramid` application, so the ACL is provided
-to each view in the :term:`context` of the request as the ``context``
-attribute.
+Every URL in a web application represents a :term:`resource` (the "R" in
+Uniform Resource Locator). Often the resource is something in your data model,
+but it could also be an abstraction over many models.
-Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to
-our :term:`Configurator` constructor, that points to the class we created
-above:
+Our wiki has two resources:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 24-25
- :linenos:
- :emphasize-lines: 2
- :lineno-start: 16
- :language: python
+#. A ``NewPage``. Represents a potential ``Page`` that does not exist. Any
+ logged-in user, having either role of ``basic`` or ``editor``, can create
+ pages.
-Only the highlighted line needs to be added.
+#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited.
+ ``editor`` users, as well as the original creator of the ``Page``, may edit
+ the ``PageResource``. Anyone may view it.
-We are now providing the ACL to the application. See :ref:`assigning_acls`
-for more information about what an :term:`ACL` represents.
+.. note::
-.. 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`` argument to :meth:`pyramid.config.Configurator.add_route`
- for more info.
+ The wiki data model is simple enough that the ``PageResource`` is mostly
+ redundant with our ``models.Page`` SQLAlchemy class. It is completely valid
+ to combine these into one class. However, for this tutorial, they are
+ explicitly separated to make clear the distinction between the parts about
+ which :app:`Pyramid` cares versus application-defined objects.
-Add authentication and authorization policies
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+There are many ways to define these resources, and they can even be grouped
+into collections with a hierarchy. However, we're keeping it simple here!
-Open ``tutorial/tutorial/__init__.py`` and add the highlighted import
-statements:
+Open the file ``tutorial/routes.py`` and edit the following lines:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 1-7
+.. literalinclude:: src/authorization/tutorial/routes.py
:linenos:
- :emphasize-lines: 2-3,7
+ :emphasize-lines: 1-11,17-
:language: python
-Now add those policies to the configuration:
+The highlighted lines need to be edited or added.
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 21-27
- :linenos:
- :lineno-start: 21
- :emphasize-lines: 1-3,6-7
- :language: python
+The ``NewPage`` class has an ``__acl__`` on it that returns a list of mappings
+from :term:`principal` to :term:`permission`. This defines *who* can do *what*
+with that :term:`resource`. In our case we want to allow only those users with
+the principals of either ``role:editor`` or ``role:basic`` to have the
+``create`` permission:
-Only the highlighted lines need to be added.
-
-We are enabling an ``AuthTktAuthenticationPolicy``, which is based in an auth
-ticket that may be included in the request. We are also enabling an
-``ACLAuthorizationPolicy``, which uses an ACL to determine the *allow* or
-*deny* outcome for a view.
-
-Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy`
-constructor accepts two arguments: ``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 the
-``groupfinder()`` function that we created before.
-
-Add permission declarations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/tutorial/views.py`` and add a ``permission='edit'`` parameter
-to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 60-61
- :emphasize-lines: 1-2
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 30-38
+ :lineno-match:
+ :emphasize-lines: 5-9
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 75-76
- :emphasize-lines: 1-2
- :language: python
-
-Only the highlighted lines, along with their preceding commas, need to be
-edited and added.
-
-The result is that only users who possess the ``edit`` permission at the time
-of the request may invoke those two views.
+The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by
+declaring a ``factory`` on the route:
-Add a ``permission='view'`` parameter to the ``@view_config`` decorator for
-``view_wiki()`` and ``view_page()`` as follows:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 30-31
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 18-19
+ :lineno-match:
:emphasize-lines: 1-2
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 36-37
- :emphasize-lines: 1-2
- :language: python
-
-Only the highlighted lines, along with their preceding commas, need to be
-edited and added.
-
-This allows anyone to invoke these two views.
-
-We are done with the changes needed to control access. The changes that
-follow will add the login and logout feature.
-
-Login, logout
--------------
-
-Add routes for /login and /logout
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Go back to ``tutorial/tutorial/__init__.py`` and add these two routes as
-highlighted:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 30-33
- :emphasize-lines: 2-3
- :language: python
-
-.. note:: The preceding lines must be added *before* the following
- ``view_page`` route definition:
-
- .. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 33
- :language: python
-
- This is because ``view_page``'s route definition uses a catch-all
- "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`)
- which will catch any route that was not already caught by any route listed
- above it in ``__init__.py``. Hence, for ``login`` and ``logout`` views to
- have the opportunity of being matched (or "caught"), they must be above
- ``/{pagename}``.
-
-Add login and logout views
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an
+actual ``Page`` object to determine *who* can do *what* to the page.
-We'll add a ``login`` view 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.
-
-Add the following import statements to the head of
-``tutorial/tutorial/views.py``:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 9-19
- :emphasize-lines: 1-11
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 47-
+ :lineno-match:
+ :emphasize-lines: 5-10
:language: python
-All the highlighted lines need to be added or edited.
-
-:meth:`~pyramid.view.forbidden_view_config` will be used to customize the
-default 403 Forbidden page. :meth:`~pyramid.security.remember` and
-:meth:`~pyramid.security.forget` help to create and expire an auth ticket
-cookie.
-
-Now add the ``login`` and ``logout`` views at the end of the file:
+The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and
+``edit_page`` routes by declaring a ``factory`` on the routes:
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 91-123
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 17-21
+ :lineno-match:
+ :emphasize-lines: 1,4-5
:language: python
-``login()`` has two decorators:
-- a ``@view_config`` decorator which associates it with the ``login`` route
- and makes it visible when we visit ``/login``,
-- a ``@forbidden_view_config`` decorator which turns it into a
- :term:`forbidden view`. ``login()`` will be invoked when a user tries to
- execute a view callable for which they lack authorization. For example, if
- a user has not logged in and tries to add or edit a Wiki page, they will be
- shown the login form before being allowed to continue.
+Add view permissions
+--------------------
-The order of these two :term:`view configuration` decorators is unimportant.
+At this point we've modified our application to load the ``PageResource``,
+including the actual ``Page`` model in the ``page_factory``. The
+``PageResource`` is now the :term:`context` for all ``view_page`` and
+``edit_page`` views. Similarly the ``NewPage`` will be the context for the
+``add_page`` view.
-``logout()`` is decorated with a ``@view_config`` decorator which associates
-it with the ``logout`` route. It will be invoked when we visit ``/logout``.
+Open the file ``tutorial/views/default.py``.
-Add the ``login.pt`` Template
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+First, you can drop a few imports that are no longer necessary:
-Create ``tutorial/tutorial/templates/login.pt`` with the following content:
-
-.. literalinclude:: src/authorization/tutorial/templates/login.pt
- :language: html
-
-The above template is referenced in the login view that we just added in
-``views.py``.
-
-Return a ``logged_in`` flag to the renderer
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Open ``tutorial/tutorial/views.py`` again. Add a ``logged_in`` parameter to
-the return value of ``view_page()``, ``edit_page()``, and ``add_page()`` as
-follows:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 57-58
- :emphasize-lines: 1-2
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 5-7
+ :lineno-match:
+ :emphasize-lines: 1
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 72-73
- :emphasize-lines: 1-2
- :language: python
+Edit the ``view_page`` view to declare the ``view`` permission, and remove the
+explicit checks within the view:
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 85-89
- :emphasize-lines: 3-4
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 18-23
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
-
-The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if
-the user is not authenticated, or a userid if the user is authenticated.
-
-Add a "Logout" link when logged in
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The work of loading the page has already been done in the factory, so we can
+just pull the ``page`` object out of the ``PageResource``, loaded as
+``request.context``. Our factory also guarantees we will have a ``Page``, as it
+raises the ``HTTPNotFound`` exception if no ``Page`` exists, again simplifying
+the view logic.
-Open ``tutorial/tutorial/templates/edit.pt`` and
-``tutorial/tutorial/templates/view.pt`` and add the following code as
-indicated by the highlighted lines.
+Edit the ``edit_page`` view to declare the ``edit`` permission:
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :lines: 34-38
- :emphasize-lines: 3-5
- :language: html
-
-The attribute ``tal:condition="logged_in"`` will make the element be included
-when ``logged_in`` is any user id. The link will invoke the logout view. The
-above element will not be included if ``logged_in`` is ``None``, such as when
-a user is not authenticated.
-
-Reviewing our changes
----------------------
-
-Our ``tutorial/tutorial/__init__.py`` will look like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :linenos:
- :emphasize-lines: 2-3,7,21-23,25-27,31-32
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 38-42
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
+Edit the ``add_page`` view to declare the ``create`` permission:
-Our ``tutorial/tutorial/models.py`` will look like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/models.py
- :linenos:
- :emphasize-lines: 1-4,33-37
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 52-56
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/views.py`` will look like this when we're done:
+Note the ``pagename`` here is pulled off of the context instead of
+``request.matchdict``. The factory has done a lot of work for us to hide the
+actual route pattern.
-.. literalinclude:: src/authorization/tutorial/views.py
- :linenos:
- :emphasize-lines: 9-11,14-19,25,31,37,58,61,73,76,88,91-117,119-123
- :language: python
+The ACLs defined on each :term:`resource` are used by the :term:`authorization
+policy` to determine if any :term:`principal` is allowed to have some
+:term:`permission`. If this check fails (for example, the user is not logged
+in) then an ``HTTPForbidden`` exception will be raised automatically. Thus
+we're able to drop those exceptions and checks from the views themselves.
+Rather we've defined them in terms of operations on a resource.
-Only the highlighted lines need to be added or edited.
+The final ``tutorial/views/default.py`` should look like the following:
-Our ``tutorial/tutorial/templates/edit.pt`` template will look like this when
-we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :linenos:
- :emphasize-lines: 36-38
- :language: html
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/templates/view.pt`` template will look like this when
-we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/view.pt
+.. literalinclude:: src/authorization/tutorial/views/default.py
:linenos:
- :emphasize-lines: 36-38
- :language: html
-
-Only the highlighted lines need to be added or edited.
+ :language: python
Viewing the application in a browser
------------------------------------
@@ -386,21 +232,32 @@ following URLs, checking that the result is as expected:
is executable by any user.
- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
- ``FrontPage`` page object.
-
-- http://localhost:6543/FrontPage/edit_page 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.
-
-- http://localhost:6543/add_page/SomePageName 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.
+ ``FrontPage`` page object. There is a "Login" link in the upper right corner
+ while the user is not authenticated, else it is a "Logout" link when the user
+ is authenticated.
+
+- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for
+ the ``FrontPage`` page object. It is executable by only the ``editor`` user.
+ If a different user (or the anonymous user) invokes it, then a login form
+ will be displayed. Supplying the credentials with the username ``editor`` and
+ password ``editor`` will display the edit page form.
+
+- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for
+ a page. If the page already exists, then it redirects the user to the
+ ``edit_page`` view for the page object. It is executable by either the
+ ``editor`` or ``basic`` user. If a different user (or the anonymous user)
+ invokes it, then a login form will be displayed. Supplying the credentials
+ with either the username ``editor`` and password ``editor``, or username
+ ``basic`` and password ``basic``, will display the edit page form.
+
+- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view
+ for an existing page, or generates an error if the page does not exist. It is
+ editable by the ``basic`` user if the page was created by that user in the
+ previous step. If, instead, the page was created by the ``editor`` user, then
+ the login page should be shown for the ``basic`` user.
- After logging 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.
+ 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, redirected
+ back to the front page, and a "Login" link is shown in the upper right hand
+ corner.
diff --git a/docs/tutorials/wiki2/background.rst b/docs/tutorials/wiki2/background.rst
index b8afb8305..2dac847d8 100644
--- a/docs/tutorials/wiki2/background.rst
+++ b/docs/tutorials/wiki2/background.rst
@@ -5,13 +5,13 @@ Background
This version of the :app:`Pyramid` wiki tutorial presents a
:app:`Pyramid` application that uses technologies which will be
familiar to someone with SQL database experience. It uses
-:term:`SQLAlchemy` as a persistence mechanism and :term:`url dispatch` to map
+: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* a Windows system of any kind.
+variant, etc.) *or* a Windows system of any kind.
.. note::
diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst
index 695d7f15b..1310e0969 100644
--- a/docs/tutorials/wiki2/basiclayout.rst
+++ b/docs/tutorials/wiki2/basiclayout.rst
@@ -12,230 +12,237 @@ Application configuration with ``__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. We use ``__init__.py`` both as a marker, indicating the directory
-in which it's contained is a package, and to contain application configuration
+package. We use ``__init__.py`` both as a marker, indicating the directory in
+which it's contained is a package, and to contain application configuration
code.
-Open ``tutorial/tutorial/__init__.py``. It should already contain
-the following:
+Open ``tutorial/__init__.py``. It should already contain the following:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :linenos:
+ :language: py
-Let's go over this piece-by-piece. First, we need some imports to support
-later code:
+Let's go over this piece-by-piece. First we need some imports to support later
+code:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :end-before: main
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :end-before: main
+ :linenos:
+ :lineno-match:
+ :language: py
``__init__.py`` defines a function named ``main``. Here is the entirety of
the ``main`` function we've defined in our ``__init__.py``:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :pyobject: main
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :pyobject: main
+ :linenos:
+ :lineno-match:
+ :language: py
When you invoke the ``pserve development.ini`` command, the ``main`` function
above is executed. It accepts some settings and returns a :term:`WSGI`
application. (See :ref:`startup_chapter` for more about ``pserve``.)
-The main function first creates a :term:`SQLAlchemy` database engine using
-:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.`` prefixed
-settings in the ``development.ini`` file's ``[app:main]`` section.
-This will be a URI (something like ``sqlite://``):
+Next in ``main``, construct a :term:`Configurator` object:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 13
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 7
+ :lineno-match:
+ :language: py
-``main`` then initializes our SQLAlchemy session object, passing it the
-engine:
+``settings`` is passed to the ``Configurator`` as a keyword argument with the
+dictionary values passed as the ``**settings`` argument. This will be a
+dictionary of settings parsed from the ``.ini`` file, which contains
+deployment-related values, such as ``pyramid.reload_templates``,
+``sqlalchemy.url``, and so on.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 14
- :language: py
+Next include :term:`Jinja2` templating bindings so that we can use renderers
+with the ``.jinja2`` extension within our project.
-``main`` subsequently initializes our SQLAlchemy declarative ``Base`` object,
-assigning the engine we created to the ``bind`` attribute of it's
-``metadata`` object. This allows table definitions done imperatively
-(instead of declaratively, via a class statement) to work. We won't use any
-such tables in our application, but if you add one later, long after you've
-forgotten about this tutorial, you won't be left scratching your head when it
-doesn't work.
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 8
+ :lineno-match:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 15
- :language: py
+Next include the the package ``models`` using a dotted Python path. The exact
+setup of the models will be covered later.
-The next step of ``main`` is to construct a :term:`Configurator` object:
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 9
+ :lineno-match:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 16
- :language: py
+Next include the ``routes`` module using a dotted Python path. This module will
+be explained in the next section.
-``settings`` is passed to the Configurator as a keyword argument with the
-dictionary values passed as the ``**settings`` argument. This will be a
-dictionary of settings parsed from the ``.ini`` file, which contains
-deployment-related values such as ``pyramid.reload_templates``,
-``db_string``, etc.
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 10
+ :lineno-match:
+ :language: py
-Next, include :term:`Chameleon` templating bindings so that we can use
-renderers with the ``.pt`` extension within our project.
+.. note::
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 17
- :language: py
+ Pyramid's :meth:`pyramid.config.Configurator.include` method is the primary
+ mechanism for extending the configurator and breaking your code into
+ feature-focused modules.
+
+``main`` next calls the ``scan`` method of the configurator
+(:meth:`pyramid.config.Configurator.scan`), which will recursively scan our
+``tutorial`` package, looking for ``@view_config`` and other special
+decorators. When it finds a ``@view_config`` decorator, a view configuration
+will be registered, allowing one of our application URLs to be mapped to some
+code.
-``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with
-two arguments: ``static`` (the name), and ``static`` (the path):
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 11
+ :lineno-match:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 18
- :language: py
+Finally ``main`` is finished configuring things, so it uses the
+:meth:`pyramid.config.Configurator.make_wsgi_app` method to return a
+:term:`WSGI` application:
+
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 12
+ :lineno-match:
+ :language: py
+
+
+Route declarations
+------------------
+
+Open the ``tutorials/routes.py`` file. It should already contain the following:
+
+.. literalinclude:: src/basiclayout/tutorial/routes.py
+ :linenos:
+ :language: py
+
+On line 2, we call :meth:`pyramid.config.Configurator.add_static_view` with
+three arguments: ``static`` (the name), ``static`` (the path), and
+``cache_max_age`` (a keyword argument).
This registers a static resource view which will match any URL that starts
with the prefix ``/static`` (by virtue of the first argument to
-``add_static_view``). This will serve up static resources for us from within
-the ``static`` directory of our ``tutorial`` package, in this case, via
+``add_static_view``). This will serve up static resources for us from within
+the ``static`` directory of our ``tutorial`` package, in this case via
``http://localhost:6543/static/`` and below (by virtue of the second argument
to ``add_static_view``). 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
+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.
-Using the configurator ``main`` also registers a :term:`route configuration`
-via the :meth:`pyramid.config.Configurator.add_route` method that will be
-used when the URL is ``/``:
+On line 3, the module registers a :term:`route configuration` via the
+:meth:`pyramid.config.Configurator.add_route` method that will be used when the
+URL is ``/``. Since this route has a ``pattern`` equaling ``/``, it is the
+route that will be matched when the URL ``/`` is visited, e.g.,
+``http://localhost:6543/``.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 19
- :language: py
-Since this route has a ``pattern`` equaling ``/`` it is the route that will
-be matched when the URL ``/`` is visited, e.g. ``http://localhost:6543/``.
-
-``main`` next calls the ``scan`` method of the configurator
-(:meth:`pyramid.config.Configurator.scan`), which will recursively scan our
-``tutorial`` package, looking for ``@view_config`` (and
-other special) decorators. When it finds a ``@view_config`` decorator, a
-view configuration will be registered, which will allow one of our
-application URLs to be mapped to some code.
-
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 20
- :language: py
-
-Finally, ``main`` is finished configuring things, so it uses the
-:meth:`pyramid.config.Configurator.make_wsgi_app` method to return a
-:term:`WSGI` application:
-
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 21
- :language: py
-
-View declarations via ``views.py``
-----------------------------------
+View declarations via the ``views`` package
+-------------------------------------------
The main function of a web framework is mapping each URL pattern to code (a
:term:`view callable`) that is executed when the requested URL matches the
corresponding :term:`route`. Our application uses the
:meth:`pyramid.view.view_config` decorator to perform this mapping.
-Open ``tutorial/tutorial/views.py``. It should already contain the following:
+Open ``tutorial/views/default.py`` in the ``views`` package. It should already
+contain the following:
- .. literalinclude:: src/basiclayout/tutorial/views.py
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/views/default.py
+ :linenos:
+ :language: py
The important part here is that the ``@view_config`` decorator associates the
-function it decorates (``my_view``) with a :term:`view configuration`,
+function it decorates (``my_view``) with a :term:`view configuration`,
consisting of:
* a ``route_name`` (``home``)
- * a ``renderer``, which is a template from the ``templates`` subdirectory
- of the package.
+ * a ``renderer``, which is a template from the ``templates`` subdirectory of
+ the package.
When the pattern associated with the ``home`` view is matched during a request,
-``my_view()`` will be executed. ``my_view()`` returns a dictionary; the
-renderer will use the ``templates/mytemplate.pt`` template to create a response
-based on the values in the dictionary.
+``my_view()`` will be executed. ``my_view()`` returns a dictionary; the
+renderer will use the ``templates/mytemplate.jinja2`` template to create a
+response based on the values in the dictionary.
Note that ``my_view()`` accepts a single argument named ``request``. This is
the standard call signature for a Pyramid :term:`view callable`.
Remember in our ``__init__.py`` when we executed the
-:meth:`pyramid.config.Configurator.scan` method ``config.scan()``? The
-purpose of calling the scan method was to find and process this
-``@view_config`` decorator in order to create a view configuration within our
-application. Without being processed by ``scan``, the decorator effectively
-does nothing. ``@view_config`` is inert without being detected via a
-:term:`scan`.
-
-The sample ``my_view()`` created by the scaffold uses a ``try:`` and ``except:``
-clause to detect if there is a problem accessing the project database and
-provide an alternate error response. That response will include the text
-shown at the end of the file, which will be displayed in the browser to
-inform the user about possible actions to take to solve the problem.
-
-Content Models with ``models.py``
----------------------------------
-
-In a SQLAlchemy-based application, a *model* object is an object composed by
-querying the SQL database. The ``models.py`` file is where the ``alchemy``
-scaffold put the classes that implement our models.
+:meth:`pyramid.config.Configurator.scan` method ``config.scan()``? The purpose
+of calling the scan method was to find and process this ``@view_config``
+decorator in order to create a view configuration within our application.
+Without being processed by ``scan``, the decorator effectively does nothing.
+``@view_config`` is inert without being detected via a :term:`scan`.
-Open ``tutorial/tutorial/models.py``. It should already contain the following:
+The sample ``my_view()`` created by the scaffold uses a ``try:`` and
+``except:`` clause to detect if there is a problem accessing the project
+database and provide an alternate error response. That response will include
+the text shown at the end of the file, which will be displayed in the browser
+to inform the user about possible actions to take to solve the problem.
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :linenos:
- :language: py
-Let's examine this in detail. First, we need some imports to support later code:
+Content models with the ``models`` package
+------------------------------------------
+
+In an SQLAlchemy-based application, a *model* object is an object composed by
+querying the SQL database. The ``models`` package is where the ``alchemy``
+scaffold put the classes that implement our models.
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :end-before: DBSession
- :linenos:
- :language: py
+First, open ``tutorial/models/meta.py``, which should already contain the
+following:
-Next we set up a SQLAlchemy ``DBSession`` object:
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :linenos:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :lines: 17
- :language: py
+``meta.py`` contains imports and support code for defining the models. We
+create a dictionary ``NAMING_CONVENTION`` as well for consistent naming of
+support objects like indices and constraints.
-``scoped_session`` and ``sessionmaker`` are standard SQLAlchemy helpers.
-``scoped_session`` allows us to access our database connection globally.
-``sessionmaker`` creates a database session object. We pass to
-``sessionmaker`` the ``extension=ZopeTransactionExtension()`` extension
-option in order to allow the system to automatically manage database
-transactions. With ``ZopeTransactionExtension`` activated, our application
-will automatically issue a transaction commit after every request unless an
-exception is raised, in which case the transaction will be aborted.
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :end-before: metadata
+ :linenos:
+ :language: py
-We also need to create a declarative ``Base`` object to use as a
-base class for our model:
+Next we create a ``metadata`` object from the class
+:class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value
+for the ``naming_convention`` argument.
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :lines: 18
- :language: py
+A ``MetaData`` object represents the table and other schema definitions for a
+single database. We also need to create a declarative ``Base`` object to use as
+a base class for our models. Our models will inherit from this ``Base``, which
+will attach the tables to the ``metadata`` we created, and define our
+application's database schema.
-Our model classes will inherit from this ``Base`` class so they can be
-associated with our particular database connection.
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :lines: 15-16
+ :lineno-match:
+ :linenos:
+ :language: py
-To give a simple example of a model class, we define one named ``MyModel``:
+Next open ``tutorial/models/mymodel.py``, which should already contain the
+following:
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :pyobject: MyModel
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
+ :linenos:
+ :language: py
+
+Notice we've defined the ``models`` as a package to make it straightforward for
+defining models in separate modules. To give a simple example of a model class,
+we have defined one named ``MyModel`` in ``mymodel.py``:
+
+.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
+ :pyobject: MyModel
+ :lineno-match:
+ :linenos:
+ :language: py
Our example model does not require an ``__init__`` method because SQLAlchemy
-supplies for us a default constructor if one is not already present,
-which accepts keyword arguments of the same name as that of the mapped attributes.
+supplies for us a default constructor, if one is not already present, which
+accepts keyword arguments of the same name as that of the mapped attributes.
.. note:: Example usage of MyModel:
@@ -247,8 +254,83 @@ The ``MyModel`` class has a ``__tablename__`` attribute. This informs
SQLAlchemy which table to use to store the data representing instances of this
class.
-The Index import and the Index object creation is not required for this
-tutorial, and will be removed in the next step.
+Finally, open ``tutorial/models/__init__.py``, which should already
+contain the following:
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :linenos:
+ :language: py
+
+Our ``models/__init__.py`` module defines the primary API we will use for
+configuring the database connections within our application, and it contains
+several functions we will cover below.
+
+As we mentioned above, the purpose of the ``models.meta.metadata`` object is to
+describe the schema of the database. This is done by defining models that
+inherit from the ``Base`` object attached to that ``metadata`` object. In
+Python, code is only executed if it is imported, and so to attach the
+``models`` table defined in ``mymodel.py`` to the ``metadata``, we must import
+it. If we skip this step, then later, when we run
+:meth:`sqlalchemy.schema.MetaData.create_all`, the table will not be created
+because the ``metadata`` object does not know about it!
+
+Another important reason to import all of the models is that, when defining
+relationships between models, they must all exist in order for SQLAlchemy to
+find and build those internal mappings. This is why, after importing all the
+models, we explicitly execute the function
+:func:`sqlalchemy.orm.configure_mappers`, once we are sure all the models have
+been defined and before we start creating connections.
+
+Next we define several functions for connecting to our database. The first and
+lowest level is the ``get_engine`` function. This creates an :term:`SQLAlchemy`
+database engine using :func:`sqlalchemy.engine_from_config` from the
+``sqlalchemy.``-prefixed settings in the ``development.ini`` file's
+``[app:main]`` section. This setting is a URI (something like ``sqlite://``).
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :pyobject: get_engine
+ :lineno-match:
+ :linenos:
+ :language: py
+
+The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database
+engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` class
+:class:`sqlalchemy.orm.session.sessionmaker`. This ``session_factory`` is then
+used for creating sessions bound to the database engine.
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :pyobject: get_session_factory
+ :lineno-match:
+ :linenos:
+ :language: py
+
+The function ``get_tm_session`` registers a database session with a transaction
+manager, and returns a ``dbsession`` object. With the transaction manager, our
+application will automatically issue a transaction commit after every request,
+unless an exception is raised, in which case the transaction will be aborted.
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :pyobject: get_tm_session
+ :lineno-match:
+ :linenos:
+ :language: py
+
+Finally, we define an ``includeme`` function, which is a hook for use with
+:meth:`pyramid.config.Configurator.include` to activate code in a Pyramid
+application add-on. It is the code that is executed above when we ran
+``config.include('.models')`` in our application's ``main`` function. This
+function will take the settings from the application, create an engine, and
+define a ``request.dbsession`` property, which we can use to do work on behalf
+of an incoming request to our application.
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :pyobject: includeme
+ :lineno-match:
+ :linenos:
+ :language: py
That's about all there is to it regarding models, views, and initialization
code in our stock application.
+
+The ``Index`` import and the ``Index`` object creation in ``mymodel.py`` is
+not required for this tutorial, and will be removed in the next step.
diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst
index b2d9bf83a..14099582c 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -3,125 +3,256 @@ Defining the Domain Model
=========================
The first change we'll make to our stock ``pcreate``-generated application will
-be to define a :term:`domain model` constructor representing a wiki page.
-We'll do this inside our ``models.py`` file.
+be to define a wiki page :term:`domain model`.
+.. note::
-Edit ``models.py``
-------------------
+ There is nothing special about the filename ``user.py`` or ``page.py`` except
+ that they are Python modules. A project may have many models throughout its
+ codebase in arbitrarily named modules. Modules implementing models often
+ have ``model`` in their names or they may live in a Python subpackage of
+ your application package named ``models`` (as we've done in this tutorial),
+ but this is only a convention and not a requirement.
-.. note::
- There is nothing 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.
+Declaring dependencies in our ``setup.py`` file
+===============================================
-Open ``tutorial/tutorial/models.py`` file and edit it to look like the
-following:
+The models 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 ``pcreate`` command; it doesn't know about our
+custom application requirements.
+
+We need to add a dependency, the ``bcrypt`` package, to our ``tutorial``
+package's ``setup.py`` file by assigning this dependency to the ``requires``
+parameter in the ``setup()`` function.
+
+Open ``tutorial/setup.py`` and edit it to look like the following:
+
+.. literalinclude:: src/models/setup.py
+ :linenos:
+ :emphasize-lines: 12
+ :language: python
+
+Only the highlighted line needs to be added.
+
+
+Running ``setup.py develop``
+============================
+
+Since a new software dependency was added, you will need to run ``python
+setup.py develop`` again inside the root of the ``tutorial`` package to obtain
+and register the newly added dependency distribution.
+
+Make sure your current working directory is the root of the project (the
+directory in which ``setup.py`` lives) and execute the following command.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ cd tutorial
+ $ $VENV/bin/python setup.py develop
+
+On Windows:
+
+.. code-block:: ps1con
+
+ c:\pyramidtut> cd tutorial
+ c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+
+Success executing this command will end with a line to the console something
+like this::
+
+ Finished processing dependencies for tutorial==0.0
+
+
+Remove ``mymodel.py``
+---------------------
+
+Let's delete the file ``tutorial/models/mymodel.py``. The ``MyModel`` class is
+only a sample and we're not going to use it.
+
+
+Add ``user.py``
+---------------
+
+Create a new file ``tutorial/models/user.py`` with the following contents:
-.. literalinclude:: src/models/tutorial/models.py
+.. literalinclude:: src/models/tutorial/models/user.py
:linenos:
:language: py
- :emphasize-lines: 20-22,24,25
-The highlighted lines are the ones that need to be changed, as well as
-removing lines that reference ``Index``.
+This is a very basic model for a user who can authenticate with our wiki.
-The first thing we've done is remove the stock ``MyModel`` class
-from the generated ``models.py`` file. The ``MyModel`` class is only a
-sample and we're not going to use it.
+We discussed briefly in the previous chapter that our models will inherit from
+an SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This will
+attach the model to our schema.
-Then, we added a ``Page`` class. Because this is a SQLAlchemy application,
-this class inherits from an instance of
-:func:`sqlalchemy.ext.declarative.declarative_base`.
+As you can see, our ``User`` class has a class-level attribute
+``__tablename__`` which equals the string ``users``. Our ``User`` class will
+also have class-level attributes named ``id``, ``name``, ``password_hash``,
+and ``role`` (all instances of :class:`sqlalchemy.schema.Column`). These will
+map to columns in the ``users`` table. The ``id`` attribute will be the primary
+key in the table. The ``name`` attribute will be a text column, each value of
+which needs to be unique within the column. The ``password_hash`` is a nullable
+text attribute that will contain a securely hashed password [1]_. Finally, the
+``role`` text attribute will hold the role of the user.
-.. literalinclude:: src/models/tutorial/models.py
- :pyobject: Page
+There are two helper methods that will help us later when using the user
+objects. The first is ``set_password`` which will take a raw password and
+transform it using bcrypt_ into an irreversible representation, a process known
+as "hashing". The second method, ``check_password``, will allow us to compare
+the hashed value of the submitted password against the hashed value of the
+password stored in the user's record in the database. If the two hashed values
+match, then the submitted password is valid, and we can authenticate the user.
+
+We hash passwords so that it is impossible to decrypt them and use them to
+authenticate in the application. If we stored passwords foolishly in clear
+text, then anyone with access to the database could retrieve any password to
+authenticate as any user.
+
+
+Add ``page.py``
+---------------
+
+Create a new file ``tutorial/models/page.py`` with the following contents:
+
+.. literalinclude:: src/models/tutorial/models/page.py
:linenos:
- :language: python
+ :language: py
-As you can see, our ``Page`` class has 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``, ``name``
-and ``data`` (all instances of :class:`sqlalchemy.schema.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.
+As you can see, our ``Page`` class is very similar to the ``User`` defined
+above, except with attributes focused on storing information about a wiki page,
+including ``id``, ``name``, and ``data``. The only new construct introduced
+here is the ``creator_id`` column, which is a foreign key referencing the
+``users`` table. Foreign keys are very useful at the schema-level, but since we
+want to relate ``User`` objects with ``Page`` objects, we also define a
+``creator`` attribute as an ORM-level mapping between the two tables.
+SQLAlchemy will automatically populate this value using the foreign key
+referencing the user. Since the foreign key has ``nullable=False``, we are
+guaranteed that an instance of ``page`` will have a corresponding
+``page.creator``, which will be a ``User`` instance.
-Changing ``scripts/initializedb.py``
-------------------------------------
+
+Edit ``models/__init__.py``
+---------------------------
+
+Since we are using a package for our models, we also need to update our
+``__init__.py`` file to ensure that the models are attached to the metadata.
+
+Open the ``tutorial/models/__init__.py`` file and edit it to look like
+the following:
+
+.. literalinclude:: src/models/tutorial/models/__init__.py
+ :linenos:
+ :language: py
+ :emphasize-lines: 8,9
+
+Here we align our imports with the names of the models, ``User`` and ``Page``.
+
+
+Edit ``scripts/initializedb.py``
+--------------------------------
We haven't looked at the details of this file yet, but within the ``scripts``
directory of your ``tutorial`` package is a file named ``initializedb.py``.
Code in this file is executed whenever we run the ``initialize_tutorial_db``
-command, as we did in the installation step of this tutorial.
+command, as we did in the installation step of this tutorial [2]_.
Since we've changed our model, we need to make changes to our
``initializedb.py`` script. In particular, we'll replace our import of
-``MyModel`` with one of ``Page`` and we'll change the very end of the script
-to create a ``Page`` rather than a ``MyModel`` and add it to our
-``DBSession``.
+``MyModel`` with those of ``User`` and ``Page``. We'll also change the very end
+of the script to create two ``User`` objects (``basic`` and ``editor``) as well
+as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``.
-Open ``tutorial/tutorial/scripts/initializedb.py`` and edit it to look like
-the following:
+Open ``tutorial/scripts/initializedb.py`` and edit it to look like the
+following:
.. literalinclude:: src/models/tutorial/scripts/initializedb.py
:linenos:
:language: python
- :emphasize-lines: 14,31,36
+ :emphasize-lines: 18,44-57
+
+Only the highlighted lines need to be changed.
-Only the highlighted lines need to be changed, as well as removing the lines
-referencing ``pyramid.scripts.common`` and ``options`` under the ``main``
-function.
Installing the project and re-initializing the database
-------------------------------------------------------
-Because our model has changed, in order to reinitialize the database, we need
-to rerun the ``initialize_tutorial_db`` command to pick up the changes you've
-made to both the models.py file and to the initializedb.py file. See
+Because our model has changed, and in order to reinitialize the database, we
+need to rerun the ``initialize_tutorial_db`` command to pick up the changes
+we've made to both the models.py file and to the initializedb.py file. See
:ref:`initialize_db_wiki2` for instructions.
Success will look something like this::
- 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
- 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages")
- 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
- CREATE TABLE pages (
- id INTEGER NOT NULL,
- name TEXT,
- data TEXT,
- PRIMARY KEY (id),
- UNIQUE (name)
- )
-
-
- 2015-05-24 15:34:14,545 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-24 15:34:14,546 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
- 2015-05-24 15:34:14,548 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
- 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data) VALUES (?, ?)
- 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page')
- 2015-05-24 15:34:14,550 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages")
+ 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("users")
+ 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-12 01:06:35,857 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
+ CREATE TABLE users (
+ id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ role TEXT NOT NULL,
+ password_hash TEXT,
+ CONSTRAINT pk_users PRIMARY KEY (id),
+ CONSTRAINT uq_users_name UNIQUE (name)
+ )
+
+
+ 2016-02-12 01:06:35,857 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-12 01:06:35,858 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-02-12 01:06:35,858 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
+ CREATE TABLE pages (
+ id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ data INTEGER NOT NULL,
+ creator_id INTEGER NOT NULL,
+ CONSTRAINT pk_pages PRIMARY KEY (id),
+ CONSTRAINT uq_pages_name UNIQUE (name),
+ CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id)
+ )
+
+
+ 2016-02-12 01:06:35,859 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-12 01:06:35,859 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-02-12 01:06:36,383 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
+ 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('editor', 'editor', '$2b$12$bSr5QR3wFs1LAnld7R94e.TXPj7DVoTxu2hA1kY6rm.Q3cAhD.AQO')
+ 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('basic', 'basic', '$2b$12$.v0BQK2xWEQOnywbX2BFs.qzXo5Qf9oZohGWux/MOSj6Z.pVaY2Z6')
+ 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
+ 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page', 1)
+ 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
View 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 (See
-:ref:`wiki2-start-the-application`), you'll wind
-up with a Python traceback on your console that ends with this exception:
+application successfully. If you try to start the application (see
+:ref:`wiki2-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
This will also happen if you attempt to run the tests.
+
+.. _bcrypt: https://pypi.python.org/pypi/bcrypt
+
+.. [1] We are using the bcrypt_ package from PyPI to hash our passwords
+ securely. There are other one-way hash algorithms for passwords if
+ bcrypt is an issue on your system. Just make sure that it's an
+ algorithm approved for storing passwords versus a generic one-way hash.
+
+.. [2] The command is named ``initialize_tutorial_db`` because of the mapping
+ defined in the ``[console_scripts]`` entry point of our project's
+ ``setup.py`` file.
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index 0b495445a..b0cbe7dc4 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -3,10 +3,10 @@ Defining Views
==============
A :term:`view callable` in a :app:`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.
+Python function that accepts a single parameter named :term:`request`. A view
+callable is assumed to return a :term:`response` object.
-The request object has a dictionary as an attribute named ``matchdict``. A
+The request object has a dictionary as an attribute named ``matchdict``. A
``matchdict`` maps the placeholders in the matching URL ``pattern`` to the
substrings of the path in the :term:`request` URL. For instance, if a call to
:meth:`pyramid.config.Configurator.add_route` has the pattern ``/{one}/{two}``,
@@ -14,13 +14,13 @@ and a user visits ``http://example.com/foo/bar``, our pattern would be matched
against ``/foo/bar`` and the ``matchdict`` would look like ``{'one':'foo',
'two':'bar'}``.
-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 ``pcreate`` command; it doesn't know
-about our custom application requirements.
+Adding the ``docutils`` dependency
+==================================
+
+Remember in the previous chapter we added a new dependency of the ``bcrypt``
+package. Again, the view code in our application will depend on a package which
+is not a dependency of the original "tutorial" application.
We need to add a dependency on the ``docutils`` package to our ``tutorial``
package's ``setup.py`` file by assigning this dependency to the ``requires``
@@ -30,109 +30,164 @@ Open ``tutorial/setup.py`` and edit it to look like the following:
.. literalinclude:: src/views/setup.py
:linenos:
- :emphasize-lines: 20
+ :emphasize-lines: 13
:language: python
Only the highlighted line needs to be added.
-Running ``setup.py develop``
-============================
+Again, as we did in the previous chapter, the dependency now needs to be
+installed, so re-run the ``python setup.py develop`` command.
+
+
+Static assets
+-------------
+
+Our templates name static assets, including CSS and images. We don't need
+to create these files within our package's ``static`` directory because they
+were provided at the time we created the project.
+
+As an example, the CSS file will be accessed via
+``http://localhost:6543/static/theme.css`` by virtue of the call to the
+``add_static_view`` directive we've made in the ``routes.py`` file. Any number
+and type of static assets can be placed in this directory (or subdirectories)
+and are just referred to by URL or by using the convenience method
+``static_url``, e.g., ``request.static_url('<package>:static/foo.css')`` within
+templates.
+
+
+Adding routes to ``routes.py``
+==============================
-Since a new software dependency was added, you will need to run ``python
-setup.py develop`` again inside the root of the ``tutorial`` package to obtain
-and register the newly added dependency distribution.
+This is the `URL Dispatch` tutorial, so let's start by adding some URL patterns
+to our app. Later we'll attach views to handle the URLs.
-Make sure your current working directory is the root of the project (the
-directory in which ``setup.py`` lives) and execute the following command.
+The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route`
+calls which serve to add routes to our application. 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.
-On UNIX:
+We then need to add four calls to ``add_route``. Note that the *ordering* of
+these declarations is very important. Route declarations are matched in the
+order they're registered.
-.. code-block:: text
+#. Add a declaration which maps the pattern ``/`` (signifying the root URL) to
+ the route named ``view_wiki``. In the next step, we will map it to our
+ ``view_wiki`` view callable by virtue of the ``@view_config`` decorator
+ attached to the ``view_wiki`` view function, which in turn will be indicated
+ by ``route_name='view_wiki'``.
- $ cd tutorial
- $ $VENV/bin/python setup.py develop
+#. Add a declaration which maps the pattern ``/{pagename}`` to the route named
+ ``view_page``. This is the regular view for a page. Again, in the next step,
+ we will map it to our ``view_page`` view callable by virtue of the
+ ``@view_config`` decorator attached to the ``view_page`` view function,
+ whin in turn will be indicated by ``route_name='view_page'``.
-On Windows:
+#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the
+ route named ``add_page``. This is the add view for a new page. We will map
+ it to our ``add_page`` view callable by virtue of the ``@view_config``
+ decorator attached to the ``add_page`` view function, which in turn will be
+ indicated by ``route_name='add_page'``.
-.. code-block:: text
+#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the
+ route named ``edit_page``. This is the edit view for a page. We will map it
+ to our ``edit_page`` view callable by virtue of the ``@view_config``
+ decorator attached to the ``edit_page`` view function, which in turn will be
+ indicated by ``route_name='edit_page'``.
- c:\pyramidtut> cd tutorial
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+As a result of our edits, the ``routes.py`` file should look like the
+following:
-Success executing this command will end with a line to the console something
-like::
+.. literalinclude:: src/views/tutorial/routes.py
+ :linenos:
+ :emphasize-lines: 3-6
+ :language: python
- Finished processing dependencies for tutorial==0.0
+The highlighted lines are the ones that need to be added or edited.
-Adding view functions in ``views.py``
-=====================================
+.. warning::
-It's time for a major change. Open ``tutorial/tutorial/views.py`` and edit it
-to look like the following:
+ The order of the routes is important! If you placed
+ ``/{pagename}/edit_page`` *before* ``/add_page/{pagename}``, then we would
+ never be able to add pages. This is because the first route would always
+ match a request to ``/add_page/edit_page`` whereas we want ``/add_page/..``
+ to have priority. This isn't a huge problem in this particular app because
+ wiki pages are always camel case, but it's important to be aware of this
+ behavior in your own apps.
-.. literalinclude:: src/views/tutorial/views.py
+
+Adding view functions in ``views/default.py``
+=============================================
+
+It's time for a major change. Open ``tutorial/views/default.py`` and
+edit it to look like the following:
+
+.. literalinclude:: src/views/tutorial/views/default.py
:linenos:
:language: python
- :emphasize-lines: 1-7,14,16-72
+ :emphasize-lines: 1-9,12-
The highlighted lines need to be added or edited.
-We added some imports and created a regular expression to find "WikiWords".
+We added some imports, and created a regular expression to find "WikiWords".
We got rid of the ``my_view`` view function and its decorator that was added
when we originally rendered the ``alchemy`` scaffold. It was only an example
-and isn't relevant to our application.
+and isn't relevant to our application. We also deleted the ``db_err_msg``
+string.
-Then we added four :term:`view callable` functions to our ``views.py``
-module:
+Then we added four :term:`view callable` functions to our ``views/default.py``
+module, as mentioned in the previous step:
* ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL.
* ``view_page()`` - Displays an individual page.
-* ``add_page()`` - Allows the user to add a page.
* ``edit_page()`` - Allows the user to edit a page.
+* ``add_page()`` - Allows the user to add a page.
We'll describe each one briefly in the following sections.
.. 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.
+ There is nothing special about the filename ``default.py`` exept that it is a
+ Python module. A project may have many view callables throughout its codebase
+ in arbitrarily named modules. Modules implementing view callables often have
+ ``view`` in their name (or may live in a Python subpackage of your
+ application package named ``views``, as in our case), but this is only by
+ convention, not a requirement.
+
The ``view_wiki`` view function
-------------------------------
Following is the code for the ``view_wiki`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 20-24
- :lineno-start: 20
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 17-20
+ :lineno-match:
:linenos:
:language: python
``view_wiki()`` is the :term:`default view` that gets called when a request is
-made to the root URL of our wiki. It always redirects to an URL which
+made to the root URL of our wiki. It always redirects to a URL which
represents the path to our "FrontPage".
The ``view_wiki`` view callable always redirects to the URL of a Page resource
named "FrontPage". To do so, it returns an instance of the
:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement
the :class:`pyramid.interfaces.IResponse` interface, like
-:class:`pyramid.response.Response` does). It uses the
-:meth:`pyramid.request.Request.route_url` API to construct an URL to the
+:class:`pyramid.response.Response`). It uses the
+:meth:`pyramid.request.Request.route_url` API to construct a URL to the
``FrontPage`` page (i.e., ``http://localhost:6543/FrontPage``), and uses it as
the "location" of the ``HTTPFound`` response, forming an HTTP redirect.
+
The ``view_page`` view function
-------------------------------
Here is the code for the ``view_page`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 25-45
- :lineno-start: 25
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 22-42
+ :lineno-match:
:linenos:
:language: python
@@ -141,83 +196,59 @@ Here is the code for the ``view_page`` view function and its decorator:
``Page`` model 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
+The curried function named ``add_link`` 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, ``check()`` generates a view
+page with the matched WikiWord name, ``add_link()`` generates a view
link to be used as the substitution value and returns it. If the wiki does
-not already contain a page with the matched WikiWord name, ``check()``
+not already contain a page with the matched WikiWord name, ``add_link()``
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
+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
``view_page()`` returns a dictionary (as opposed to a :term:`response` object)
is a cue to :app:`Pyramid` that it should try to use a :term:`renderer`
associated with the view configuration to render a response. In our case, the
-renderer used will be the ``templates/view.pt`` template, as indicated in the
-``@view_config`` decorator that is applied to ``view_page()``.
+renderer used will be the ``view.jinja2`` template, as indicated in
+the ``@view_config`` decorator that is applied to ``view_page()``.
-The ``add_page`` view function
-------------------------------
+If the page does not exist, then we need to handle that by raising a
+:class:`pyramid.httpexceptions.HTTPNotFound` to trigger our 404 handling,
+defined in ``tutorial/views/notfound.py``.
-Here is the code for the ``add_page`` view function and its decorator:
-
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 47-58
- :lineno-start: 47
- :linenos:
- :language: python
-
-``add_page()`` is 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.
-``add_page()`` 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 value for
-``'pagename'`` in the ``matchdict`` will be ``'SomeName'``.
+.. note::
-If the view execution *is* a result of a form submission (i.e., the expression
-``'form.submitted' in request.params`` is ``True``), we grab the page body
-from the form data, create a Page object with this page body and the name
-taken from ``matchdict['pagename']``, and save it into the database using
-``DBSession.add``. We then redirect back to the ``view_page`` view for the
-newly created page.
+ Using ``raise`` versus ``return`` with the HTTP exceptions is an important
+ distinction that can commonly mess people up. In
+ ``tutorial/views/notfound.py`` there is an :term:`exception view`
+ registered for handling the ``HTTPNotFound`` exception. Exception views are
+ only triggered for raised exceptions. If the ``HTTPNotFound`` is returned,
+ then it has an internal "stock" template that it will use to render itself
+ as a response. If you aren't seeing your exception view being executed, this
+ is most likely the problem! See :ref:`special_exceptions_in_callables` for
+ more information about exception views.
-If the view execution is *not* a result of a form submission (i.e., 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 uses as the form post URL during rendering. We're lazy here, so
-we're going to use the same template (``templates/edit.pt``) for the add
-view as well as the page edit view. To do so we create a dummy Page object
-in order to satisfy the edit form's desire to have *some* page object
-exposed as ``page``. :app:`Pyramid` will render the template associated
-with this view to a response.
The ``edit_page`` view function
-------------------------------
Here is the code for the ``edit_page`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 60-72
- :lineno-start: 60
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 44-56
+ :lineno-match:
:linenos:
:language: python
-``edit_page()`` is 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 ``edit_page`` view will have a ``'pagename'`` key
-matching the name of the page the user wants to edit.
+``edit_page()`` is 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 which it renders. The ``matchdict`` attribute of the request passed to the
+``edit_page`` view will have a ``'pagename'`` key matching the name of the page
+that the user wants to edit.
If the view execution *is* a result of a form submission (i.e., the expression
``'form.submitted' in request.params`` is ``True``), the view grabs the
@@ -230,120 +261,190 @@ expression ``'form.submitted' in request.params`` is ``False``), the view
simply renders the edit form, passing the page object and a ``save_url``
which will be used as the action of the generated form.
+.. note::
+
+ Since our ``request.dbsession`` defined in the previous chapter is
+ registered with the ``pyramid_tm`` transaction manager, any changes we make
+ to objects managed by the that session will be committed automatically. In
+ the event that there was an error (even later, in our template code), the
+ changes would be aborted. This means the view itself does not need to
+ concern itself with commit/rollback logic.
+
+
+The ``add_page`` view function
+------------------------------
+
+Here is the code for the ``add_page`` view function and its decorator:
+
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 58-
+ :lineno-match:
+ :linenos:
+ :language: python
+
+``add_page()`` is invoked when a user clicks on a *WikiWord* which isn't yet
+represented as a page in the system. The ``add_link`` function within the
+``view_page`` view generates URLs to this view. ``add_page()`` 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, for example,
+``http://localhost:6543/add_page/SomeName``, the value for ``'pagename'`` in
+the ``matchdict`` will be ``'SomeName'``.
+
+Next a check is performed to determine whether the ``Page`` already exists in
+the database. If it already exists, then the client is redirected to the
+``edit_page`` view, else we continue to the next check.
+
+If the view execution *is* a result of a form submission (i.e., the expression
+``'form.submitted' in request.params`` is ``True``), we grab the page body from
+the form data, create a Page object with this page body and the name taken from
+``matchdict['pagename']``, and save it into the database using
+``request.dbession.add``. Since we have not yet covered authentication, we
+don't have a logged-in user to add as the page's ``creator``. Until we get to
+that point in the tutorial, we'll just assume that all pages are created by the
+``editor`` user. Thus we query for that object, and set it on ``page.creator``.
+Finally, we redirect the client back to the ``view_page`` view for the newly
+created page.
+
+If the view execution is *not* a result of a form submission (i.e., 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 uses as the form post URL during rendering. We're lazy here, so
+we're going to use the same template (``templates/edit.jinja2``) for the add
+view as well as the page edit view. To do so we create a dummy ``Page`` object
+in order to satisfy the edit form's desire to have *some* page object
+exposed as ``page``. :app:`Pyramid` will render the template associated
+with this view to a response.
+
+
Adding templates
================
The ``view_page``, ``add_page`` and ``edit_page`` views that we've added
-reference a :term:`template`. Each template is a :term:`Chameleon`
-:term:`ZPT` template. These templates will live in the ``templates``
-directory of our tutorial package. Chameleon templates must have a ``.pt``
-extension to be recognized as such.
+reference a :term:`template`. Each template is a :term:`Jinja2` template.
+These templates will live in the ``templates`` directory of our tutorial
+package. Jinja2 templates must have a ``.jinja2`` extension to be recognized
+as such.
+
-The ``view.pt`` template
-------------------------
+The ``layout.jinja2`` template
+------------------------------
-Create ``tutorial/tutorial/templates/view.pt`` and add the following
-content:
+Update ``tutorial/templates/layout.jinja2`` with the following content, as
+indicated by the emphasized lines:
-.. literalinclude:: src/views/tutorial/templates/view.pt
+.. literalinclude:: src/views/tutorial/templates/layout.jinja2
:linenos:
+ :emphasize-lines: 11,36
:language: html
-This template is used by ``view_page()`` for displaying a single
-wiki page. It includes:
+Since we're using a templating engine, we can factor common boilerplate out of
+our page templates into reusable components. One method for doing this is
+template inheritance via blocks.
-- A ``div`` element that is replaced with the ``content`` value provided by
- the view (lines 36-38). ``content`` contains HTML, so the ``structure``
- keyword is used to prevent escaping it (i.e., changing ">" to "&gt;", etc.)
-- A link that points at the "edit" URL which invokes the ``edit_page`` view
- for the page being viewed (lines 40-42).
+- We have defined two placeholders in the layout template where a child
+ template can override the content. These blocks are named ``subtitle`` (line
+ 11) and ``content`` (line 36).
+- Please refer to the Jinja2_ documentation for more information about template
+ inheritance.
-The ``edit.pt`` template
-------------------------
-Create ``tutorial/tutorial/templates/edit.pt`` and add the following
-content:
+The ``view.jinja2`` template
+----------------------------
-.. literalinclude:: src/views/tutorial/templates/edit.pt
+Create ``tutorial/templates/view.jinja2`` and add the following content:
+
+.. literalinclude:: src/views/tutorial/templates/view.jinja2
:linenos:
:language: html
-This template is used by ``add_page()`` and ``edit_page()`` for adding and
-editing a wiki page. It displays a page containing a form that includes:
+This template is used by ``view_page()`` for displaying a single wiki page.
-- A 10 row by 60 column ``textarea`` field named ``body`` that is filled
- with any existing page data when it is rendered (line 45).
-- A submit button that has the name ``form.submitted`` (line 48).
+- We begin by extending the ``layout.jinja2`` template defined above, which
+ provides the skeleton of the page (line 1).
+- We override the ``subtitle`` block from the base layout, inserting the page
+ name into the page's title (line 3).
+- We override the ``content`` block from the base layout to insert our markup
+ into the body (lines 5-18).
+- We use a variable that is replaced with the ``content`` value provided by the
+ view (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to
+ prevent escaping it (e.g., changing ">" to "&gt;").
+- We create a link that points at the "edit" URL, which when clicked invokes
+ the ``edit_page`` view for the requested page (line 9).
-The form POSTs back to the ``save_url`` argument supplied by the view (line
-43). The view will use the ``body`` and ``form.submitted`` values.
-.. note:: Our templates use a ``request`` object that none of our tutorial
- views return in their dictionary. ``request`` is one of several names that
- are available "by default" in a template when a template renderer is used.
- See :ref:`renderer_system_values` for information about other names that
- are available by default when a template is used as a renderer.
+The ``edit.jinja2`` template
+----------------------------
-Static Assets
--------------
+Create ``tutorial/templates/edit.jinja2`` and add the following content:
-Our templates name static assets, including CSS and images. We don't need
-to create these files within our package's ``static`` directory because they
-were provided at the time we created the project.
+.. literalinclude:: src/views/tutorial/templates/edit.jinja2
+ :linenos:
+ :emphasize-lines: 1,3,12,14,17
+ :language: html
-As an example, the CSS file will be accessed via
-``http://localhost:6543/static/theme.css`` by virtue of the call to the
-``add_static_view`` directive we've made in the ``__init__.py`` file. Any
-number and type of static assets can be placed in this directory (or
-subdirectories) and are just referred to by URL or by using the convenience
-method ``static_url``, e.g.,
-``request.static_url('<package>:static/foo.css')`` within templates.
-
-Adding Routes to ``__init__.py``
-================================
-
-The ``__init__.py`` file contains
-:meth:`pyramid.config.Configurator.add_route` calls which serve to add routes
-to our application. 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 calls to ``add_route``. Note that the *ordering* of
-these declarations is very important. ``route`` declarations are matched in
-the order they're found in the ``__init__.py`` file.
-
-#. Add a declaration which maps the pattern ``/`` (signifying the root URL)
- to the route named ``view_wiki``. It maps to our ``view_wiki`` view
- callable by virtue of the ``@view_config`` attached to the ``view_wiki``
- view function indicating ``route_name='view_wiki'``.
+This template serves two use cases. It is used by ``add_page()`` and
+``edit_page()`` for adding and editing a wiki page. It displays a page
+containing a form and which provides the following:
-#. Add a declaration which maps the pattern ``/{pagename}`` to the route named
- ``view_page``. This is the regular view for a page. It maps
- to our ``view_page`` view callable by virtue of the ``@view_config``
- attached to the ``view_page`` view function indicating
- ``route_name='view_page'``.
+- Again, we extend the ``layout.jinja2`` template, which provides the skeleton
+ of the page (line 1).
+- Override the ``subtitle`` block to affect the ``<title>`` tag in the
+ ``head`` of the page (line 3).
+- A 10-row by 60-column ``textarea`` field named ``body`` that is filled with
+ any existing page data when it is rendered (line 14).
+- A submit button that has the name ``form.submitted`` (line 17).
+- The form POSTs back to the ``save_url`` argument supplied by the view (line
+ 12). The view will use the ``body`` and ``form.submitted`` values.
-#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the
- route named ``add_page``. This is the add view for a new page. It maps
- to our ``add_page`` view callable by virtue of the ``@view_config``
- attached to the ``add_page`` view function indicating
- ``route_name='add_page'``.
-#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the
- route named ``edit_page``. This is the edit view for a page. It maps
- to our ``edit_page`` view callable by virtue of the ``@view_config``
- attached to the ``edit_page`` view function indicating
- ``route_name='edit_page'``.
+The ``404.jinja2`` template
+---------------------------
+
+Replace ``tutorial/templates/404.jinja2`` with the following content:
+
+.. literalinclude:: src/views/tutorial/templates/404.jinja2
+ :linenos:
+ :language: html
-As a result of our edits, the ``__init__.py`` file should look
-something like:
+This template is linked from the ``notfound_view`` defined in
+``tutorial/views/notfound.py`` as shown here:
-.. literalinclude:: src/views/tutorial/__init__.py
+.. literalinclude:: src/views/tutorial/views/notfound.py
:linenos:
- :emphasize-lines: 19-22
+ :emphasize-lines: 6
:language: python
-The highlighted lines are the ones that need to be added or edited.
+There are several important things to note about this configuration:
+
+- The ``notfound_view`` in the above snippet is called an
+ :term:`exception view`. For more information see
+ :ref:`special_exceptions_in_callables`.
+- The ``notfound_view`` sets the response status to 404. It's possible
+ to affect the response object used by the renderer via
+ :ref:`request_response_attr`.
+- The ``notfound_view`` is registered as an exception view and will be invoked
+ **only** if ``pyramid.httpexceptions.HTTPNotFound`` is raised as an
+ exception. This means it will not be invoked for any responses returned
+ from a view normally. For example, on line 27 of
+ ``tutorial/views/default.py`` the exception is raised which will trigger
+ the view.
+
+Finally, we may delete the ``tutorial/templates/mytemplate.jinja2`` template
+that was provided by the ``alchemy`` scaffold, as we have created our own
+templates for the wiki.
+
+.. note::
+
+ Our templates use a ``request`` object that none of our tutorial
+ views return in their dictionary. ``request`` is one of several names that
+ are available "by default" in a template when a template renderer is used.
+ See :ref:`renderer_system_values` for information about other names that
+ are available by default when a template is used as a renderer.
+
Viewing the application in a browser
====================================
@@ -355,15 +456,22 @@ each of the following URLs, checking that the result is as expected:
- http://localhost:6543/ invokes the ``view_wiki`` view. This always
redirects to the ``view_page`` view of the ``FrontPage`` page object.
-- http://localhost:6543/FrontPage invokes the ``view_page`` view of the front
- page object.
+- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
+ ``FrontPage`` page object.
+
+- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for
+ the ``FrontPage`` page object.
-- http://localhost:6543/FrontPage/edit_page invokes the edit view for the
- front page object.
+- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for
+ a page. If the page already exists, then it redirects the user to the
+ ``edit_page`` view for the page object.
-- http://localhost:6543/add_page/SomePageName invokes the add view for a page.
+- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view
+ for an existing page, or generates an error if the page does not exist.
- To generate an error, visit http://localhost:6543/foobars/edit_page which
will generate a ``NoResultFound: No row was found for one()`` error. You'll
see an interactive traceback facility provided by
:term:`pyramid_debugtoolbar`.
+
+.. _jinja2: http://jinja.pocoo.org/
diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst
index 52f2ce7a5..ba25b0697 100644
--- a/docs/tutorials/wiki2/design.rst
+++ b/docs/tutorials/wiki2/design.rst
@@ -2,149 +2,159 @@
Design
======
-Following is a quick overview of the design of our wiki application, to help
-us understand the changes that we will be making as we work through the
-tutorial.
+Following is a quick overview of the design of our wiki application to help us
+understand the changes that we will be making as we work through the tutorial.
Overall
--------
+=======
We choose to use :term:`reStructuredText` markup in the wiki text. Translation
from reStructuredText to HTML is provided by the widely used ``docutils``
-Python module. We will add this module in the dependency list on the project
+Python module. We will add this module to the dependency list in the project's
``setup.py`` file.
Models
-------
+======
-We'll be using a SQLite database to hold our wiki data, and we'll be using
+We'll be using an SQLite database to hold our wiki data, and we'll be using
:term:`SQLAlchemy` to access the data in this database.
-Within the database, we define a single table named `pages`, whose elements
-will store the wiki pages. There are two columns: `name` and `data`.
+Within the database, we will define two tables:
+
+- The `users` table which will store the `id`, `name`, `password_hash` and
+ `role` of each wiki user.
+- The `pages` table, whose elements will store the wiki pages.
+ There are four columns: `id`, `name`, `data` and `creator_id`.
+
+There is a one-to-many relationship between `users` and `pages` tracking
+the user who created each wiki page defined by the `creator_id` column on the
+`pages` table.
-URLs like ``/PageName`` will try to find an element in
-the table that has a corresponding name.
+URLs like ``/PageName`` will try to find an element in the `pages` table that
+has a corresponding name.
-To add a page to the wiki, a new row is created and the text
-is stored in `data`.
+To add a page to the wiki, a new row is created and the text is stored in
+`data`.
A page named ``FrontPage`` containing the text *This is the front page*, will
be created when the storage is initialized, and will be used as the wiki home
page.
-Views
------
+Wiki Views
+==========
-There will be three views to handle the normal operations of adding,
-editing, and viewing wiki pages, plus one view for the wiki front page.
-Two templates will be used, one for viewing, and one for both adding
-and editing wiki pages.
+There will be three views to handle the normal operations of adding, editing,
+and viewing wiki pages, plus one view for the wiki front page. Two templates
+will be used, one for viewing, and one for both adding and editing wiki pages.
-The default templating systems in :app:`Pyramid` are
-:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of
-:term:`ZPT`, which is an XML-based templating language. Mako is a
-non-XML-based templating language. Because we had to pick one,
-we chose Chameleon for this tutorial.
+As of version 1.5 :app:`Pyramid` no longer ships with templating systems. In
+this tutorial, we will use :term:`Jinja2`. Jinja2 is a modern and
+designer-friendly templating language for Python, modeled after Django's
+templates.
Security
---------
-
-We'll eventually be adding security to our application. The components we'll
-use to do this are below.
-
-- USERS, a dictionary mapping :term:`userids <userid>` to their
- corresponding passwords.
-
-- GROUPS, a dictionary mapping :term:`userids <userid>` to a
- list of groups to which they belong.
-
-- ``groupfinder``, an *authorization callback* that looks up USERS and
- GROUPS. It will be provided in a new ``security.py`` file.
-
-- An :term:`ACL` is attached to the root :term:`resource`. Each row below
- details an :term:`ACE`:
-
- +----------+----------------+----------------+
- | Action | Principal | Permission |
- +==========+================+================+
- | Allow | Everyone | View |
- +----------+----------------+----------------+
- | Allow | group:editors | Edit |
- +----------+----------------+----------------+
-
-- Permission declarations are added to the views to assert the security
- policies as each request is handled.
-
-Two additional views and one template will handle the login and
-logout tasks.
+========
+
+We'll eventually be adding security to our application. To do this, we'll
+be using a very simple role-based security model. We'll assign a single
+role category to each user in our system.
+
+`basic`
+ An authenticated user who can view content and create new pages. A `basic`
+ user may also edit the pages they have created but not pages created by
+ other users.
+
+`editor`
+ An authenticated user who can create and edit any content in the system.
+
+In order to accomplish this we'll need to define an authentication policy
+which can identify users by their :term:`userid` and role. Then we'll
+need to define a page :term:`resource` which contains the appropriate
+:term:`ACL`:
+
++----------+--------------------+----------------+
+| Action | Principal | Permission |
++==========+====================+================+
+| Allow | Everyone | view |
++----------+--------------------+----------------+
+| Allow | group:basic | create |
++----------+--------------------+----------------+
+| Allow | group:editors | edit |
++----------+--------------------+----------------+
+| Allow | <creator of page> | edit |
++----------+--------------------+----------------+
+
+Permission declarations will be added to the views to assert the security
+policies as each request is handled.
+
+On the security side of the application there are two additional views for
+handling login and logout as well as two exception views for handling
+invalid access attempts and unhandled URLs.
Summary
--------
-
-The URL, actions, template and permission associated to each view are
-listed in the following table:
-
-+----------------------+-----------------------+-------------+------------+------------+
-| URL | Action | View | Template | Permission |
-| | | | | |
-+======================+=======================+=============+============+============+
-| / | Redirect to | view_wiki | | |
-| | /FrontPage | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /PageName | Display existing | view_page | view.pt | view |
-| | page [2]_ | [1]_ | | |
-| | | | | |
-| | | | | |
-| | | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /PageName/edit_page | Display edit form | edit_page | edit.pt | edit |
-| | with existing | | | |
-| | content. | | | |
-| | | | | |
-| | If the form was | | | |
-| | submitted, redirect | | | |
-| | to /PageName | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /add_page/PageName | Create the page | add_page | edit.pt | edit |
-| | *PageName* in | | | |
-| | storage, display | | | |
-| | the edit form | | | |
-| | without content. | | | |
-| | | | | |
-| | If the form was | | | |
-| | submitted, | | | |
-| | redirect to | | | |
-| | /PageName | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /login | Display login form, | login | login.pt | |
-| | Forbidden [3]_ | | | |
-| | | | | |
-| | If the form was | | | |
-| | submitted, | | | |
-| | authenticate. | | | |
-| | | | | |
-| | - If authentication | | | |
-| | succeeds, | | | |
-| | redirect to the | | | |
-| | page that we | | | |
-| | came from. | | | |
-| | | | | |
-| | - If authentication | | | |
-| | fails, display | | | |
-| | login form with | | | |
-| | "login failed" | | | |
-| | message. | | | |
-| | | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /logout | Redirect to | logout | | |
-| | /FrontPage | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-
-.. [1] This is the default view for a Page context
- when there is no view name.
-.. [2] Pyramid will return a default 404 Not Found page
- if the page *PageName* does not exist yet.
-.. [3] ``pyramid.exceptions.Forbidden`` is reached when a
- user tries to invoke a view that is
- not authorized by the authorization policy.
+=======
+
+The URL, actions, template, and permission associated to each view are listed
+in the following table:
+
++----------------------+-----------------------+-------------+----------------+------------+
+| URL | Action | View | Template | Permission |
+| | | | | |
++======================+=======================+=============+================+============+
+| / | Redirect to | view_wiki | | |
+| | /FrontPage | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /PageName | Display existing | view_page | view.jinja2 | view |
+| | page [2]_ | [1]_ | | |
+| | | | | |
+| | | | | |
+| | | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /PageName/edit_page | Display edit form | edit_page | edit.jinja2 | edit |
+| | with existing | | | |
+| | content. | | | |
+| | | | | |
+| | If the form was | | | |
+| | submitted, redirect | | | |
+| | to /PageName | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /add_page/PageName | Create the page | add_page | edit.jinja2 | create |
+| | *PageName* in | | | |
+| | storage, display | | | |
+| | the edit form | | | |
+| | without content. | | | |
+| | | | | |
+| | If the form was | | | |
+| | submitted, | | | |
+| | redirect to | | | |
+| | /PageName | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /login | Display login form, | login | login.jinja2 | |
+| | Forbidden [3]_ | | | |
+| | | | | |
+| | If the form was | | | |
+| | submitted, | | | |
+| | authenticate. | | | |
+| | | | | |
+| | - If authentication | | | |
+| | succeeds, | | | |
+| | redirect to the | | | |
+| | page from which | | | |
+| | we came. | | | |
+| | | | | |
+| | - If authentication | | | |
+| | fails, display | | | |
+| | login form with | | | |
+| | "login failed" | | | |
+| | message. | | | |
+| | | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /logout | Redirect to | logout | | |
+| | /FrontPage | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+
+.. [1] This is the default view for a Page context when there is no view name.
+.. [2] Pyramid will return a default 404 Not Found page if the page *PageName*
+ does not exist yet.
+.. [3] ``pyramid.exceptions.Forbidden`` is reached when a user tries to invoke
+ a view that is not authorized by the authorization policy.
diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst
index fee50a1cf..84e0e6d84 100644
--- a/docs/tutorials/wiki2/distributing.rst
+++ b/docs/tutorials/wiki2/distributing.rst
@@ -4,19 +4,18 @@ 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 :app:`Pyramid` environment.
+current working directory contains the ``tutorial`` package and the
+``setup.py`` file.
On UNIX:
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/python setup.py sdist
On Windows:
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut> %VENV%\Scripts\python setup.py sdist
@@ -27,8 +26,7 @@ The output of such a command will be something like:
running sdist
# .. more output ..
creating dist
- tar -cf dist/tutorial-0.0.tar tutorial-0.0
- gzip -f9 dist/tutorial-0.0.tar
+ Creating tar archive
removing 'tutorial-0.0' (and everything under it)
Note that this command creates a tarball in the "dist" subdirectory named
diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst
index 0a3873dcd..18e9f552e 100644
--- a/docs/tutorials/wiki2/index.rst
+++ b/docs/tutorials/wiki2/index.rst
@@ -1,15 +1,15 @@
.. _bfg_sql_wiki_tutorial:
-SQLAlchemy + URL Dispatch Wiki Tutorial
+SQLAlchemy + URL dispatch wiki tutorial
=======================================
-This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based
+This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based
:app:`Pyramid` application to a developer familiar with Python. When the
-tutorial is finished, the developer will have created a basic Wiki
-application with authentication.
+tutorial is finished, the developer will have created a basic wiki
+application with authentication and authorization.
-For cut and paste purposes, the source code for all stages of this
-tutorial can be browsed on GitHub at `docs/tutorials/wiki2/src
+For cut and paste purposes, the source code for all stages of this tutorial can
+be browsed on GitHub at `docs/tutorials/wiki2/src
<https://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src>`_,
which corresponds to the same location if you have Pyramid sources.
@@ -22,6 +22,7 @@ which corresponds to the same location if you have Pyramid sources.
basiclayout
definingmodels
definingviews
+ authentication
authorization
tests
distributing
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 595dbd940..891305bf5 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -3,7 +3,7 @@ Installation
============
Before you begin
-================
+----------------
This tutorial assumes that you have already followed the steps in
:ref:`installing_chapter`, except **do not create a virtualenv or install
@@ -13,6 +13,7 @@ Pyramid**. Thereby you will satisfy the following requirements.
* :term:`setuptools` or :term:`distribute` is installed
* :term:`virtualenv` is installed
+
Create directory to contain the project
---------------------------------------
@@ -21,55 +22,55 @@ We need a workspace for our project files.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ mkdir ~/pyramidtut
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\> mkdir pyramidtut
+
Create and use a virtual Python environment
-------------------------------------------
-Next let's create a `virtualenv` workspace for our project. We will
-use the `VENV` environment variable instead of the absolute path of the
-virtual environment.
+Next let's create a ``virtualenv`` workspace for our project. We will use the
+``VENV`` environment variable instead of the absolute path of the virtual
+environment.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ export VENV=~/pyramidtut
$ virtualenv $VENV
- New python executable in /home/foo/env/bin/python
- Installing setuptools.............done.
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\> set VENV=c:\pyramidtut
-Versions of Python use different paths, so you will need to adjust the
+Each version of Python uses different paths, so you will need to adjust the
path to the command for your Python version.
Python 2.7:
-.. code-block:: text
+.. code-block:: ps1con
c:\> c:\Python27\Scripts\virtualenv %VENV%
-Python 3.3:
+Python 3.5:
+
+.. code-block:: ps1con
-.. code-block:: text
+ c:\> c:\Python35\Scripts\virtualenv %VENV%
- c:\> c:\Python33\Scripts\virtualenv %VENV%
Install Pyramid into the virtual Python environment
---------------------------------------------------
@@ -77,293 +78,304 @@ Install Pyramid into the virtual Python environment
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/easy_install pyramid
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\> %VENV%\Scripts\easy_install pyramid
+
Install SQLite3 and its development packages
--------------------------------------------
-If you used a package manager to install your Python or if you compiled
-your Python from source, then you must install SQLite3 and its
-development packages. If you downloaded your Python as an installer
-from python.org, then you already have it installed and can proceed to
-the next section :ref:`sql_making_a_project`..
+If you used a package manager to install your Python or if you compiled your
+Python from source, then you must install SQLite3 and its development packages.
+If you downloaded your Python as an installer from https://www.python.org, then
+you already have it installed and can skip this step.
-If you need to install the SQLite3 packages, then, for example, using
-the Debian system and apt-get, the command would be the following:
+If you need to install the SQLite3 packages, then, for example, using the
+Debian system and ``apt-get``, the command would be the following:
-.. code-block:: text
+.. code-block:: bash
$ sudo apt-get install libsqlite3-dev
+
Change directory to your virtual Python environment
---------------------------------------------------
-Change directory to the ``pyramidtut`` directory.
+Change directory to the ``pyramidtut`` directory, which is both your workspace
+and your virtual environment.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ cd pyramidtut
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\> cd pyramidtut
+
.. _sql_making_a_project:
Making a project
-================
+----------------
Your next step is to create a project. For this tutorial we will use
the :term:`scaffold` named ``alchemy`` which generates an application
that uses :term:`SQLAlchemy` and :term:`URL dispatch`.
-:app:`Pyramid` supplies a variety of scaffolds to generate sample
-projects. We will use `pcreate`—a script that comes with Pyramid to
-quickly and easily generate scaffolds, usually with a single command—to
-create the scaffold for our project.
+:app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We
+will use ``pcreate``, a script that comes with Pyramid, to create our project
+using a scaffold.
-By passing `alchemy` into the `pcreate` command, the script creates
-the files needed to use SQLAlchemy. By passing in our application name
-`tutorial`, the script inserts that application name into all the
-required files. For example, `pcreate` creates the
-``initialize_tutorial_db`` in the ``pyramidtut/bin`` directory.
+By passing ``alchemy`` into the ``pcreate`` command, the script creates the
+files needed to use SQLAlchemy. By passing in our application name
+``tutorial``, the script inserts that application name into all the required
+files. For example, ``pcreate`` creates the ``initialize_tutorial_db`` in the
+``pyramidtut/bin`` directory.
The below instructions assume your current working directory is "pyramidtut".
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/pcreate -s alchemy tutorial
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial
-.. note:: If you are using Windows, the ``alchemy``
- scaffold 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.
+.. note:: If you are using Windows, the ``alchemy`` scaffold 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_project_in_dev_mode:
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 the virtualenv Python interpreter.
+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, change directory to the ``tutorial`` directory that
+you created in :ref:`sql_making_a_project`, and run the ``setup.py develop``
+command using the virtualenv Python interpreter.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ cd tutorial
$ $VENV/bin/python setup.py develop
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut> cd tutorial
c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
-The console will show `setup.py` checking for packages and installing
-missing packages. Success executing this command will show a line like
-the following::
+The console will show ``setup.py`` checking for packages and installing missing
+packages. Success executing this command will show a line like the following::
Finished processing dependencies for tutorial==0.0
.. _sql_running_tests:
Run the tests
-=============
+-------------
-After you've installed the project in development mode, you may run
-the tests for the project.
+After you've installed the project in development mode, you may run the tests
+for the project.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/python setup.py test -q
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
For a successful test run, you should see output that ends like this::
- .
- ----------------------------------------------------------------------
- Ran 1 test in 0.094s
-
- OK
+ ..
+ ----------------------------------------------------------------------
+ Ran 2 tests in 0.053s
+
+ OK
Expose 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.
+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 by the tests.
To get this functionality working, we'll need to install the ``nose`` and
``coverage`` packages into our ``virtualenv``:
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/easy_install nose coverage
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut\tutorial> %VENV%\Scripts\easy_install nose coverage
-Once ``nose`` and ``coverage`` are installed, we can actually run the
-coverage tests.
+Once ``nose`` and ``coverage`` are installed, we can run the tests with
+coverage.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \
--cover-erase --with-coverage
If successful, you will see output something like this::
- .
- Name Stmts Miss Cover Missing
- ---------------------------------------------------
- tutorial.py 13 9 31% 13-21
- tutorial/models.py 12 0 100%
- tutorial/scripts.py 0 0 100%
- tutorial/views.py 11 0 100%
- ---------------------------------------------------
- TOTAL 36 9 75%
- ----------------------------------------------------------------------
- Ran 2 tests in 0.643s
+ ..
+ Name Stmts Miss Cover Missing
+ ----------------------------------------------------------
+ tutorial.py 8 6 25% 7-12
+ tutorial/models.py 22 0 100%
+ tutorial/models/meta.py 5 0 100%
+ tutorial/models/mymodel.py 8 0 100%
+ tutorial/scripts.py 0 0 100%
+ tutorial/views.py 0 0 100%
+ tutorial/views/default.py 12 0 100%
+ ----------------------------------------------------------
+ TOTAL 55 6 89%
+ ----------------------------------------------------------------------
+ Ran 2 tests in 0.579s
+
+ OK
- OK
+Our package doesn't quite have 100% test coverage.
-Looks like our package doesn't quite have 100% test coverage.
.. _initialize_db_wiki2:
Initializing the database
-=========================
+-------------------------
+
+We need to use the ``initialize_tutorial_db`` :term:`console script` to
+initialize our database.
+
+.. note::
-We need to use the ``initialize_tutorial_db`` :term:`console
-script` to initialize our database.
+ The ``initialize_tutorial_db`` command does not perform a migration, but
+ rather it simply creates missing tables and adds some dummy data. If you
+ already have a database, you should delete it before running
+ ``initialize_tutorial_db`` again.
Type the following command, making sure you are still in the ``tutorial``
directory (the directory with a ``development.ini`` in it):
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/initialize_tutorial_db development.ini
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini
The output to your console should be something like this::
- 2015-05-23 16:49:49,609 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
- 2015-05-23 16:49:49,609 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
- 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models")
- 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-23 16:49:49,612 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
- CREATE TABLE models (
- id INTEGER NOT NULL,
- name TEXT,
- value INTEGER,
- PRIMARY KEY (id)
- )
-
-
- 2015-05-23 16:49:49,612 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
- 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name)
- 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-23 16:49:49,614 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
- 2015-05-23 16:49:49,616 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
- 2015-05-23 16:49:49,617 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?)
- 2015-05-23 16:49:49,617 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1)
- 2015-05-23 16:49:49,618 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
-
-Success! You should now have a ``tutorial.sqlite`` file in your current working
-directory. This will be a SQLite database with a single table defined in it
+ 2016-02-21 23:57:41,793 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2016-02-21 23:57:41,793 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-02-21 23:57:41,794 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2016-02-21 23:57:41,794 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-02-21 23:57:41,796 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models")
+ 2016-02-21 23:57:41,796 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
+ CREATE TABLE models (
+ id INTEGER NOT NULL,
+ name TEXT,
+ value INTEGER,
+ CONSTRAINT pk_models PRIMARY KEY (id)
+ )
+
+
+ 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name)
+ 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-02-21 23:57:41,801 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
+ 2016-02-21 23:57:41,802 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?)
+ 2016-02-21 23:57:41,802 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1)
+ 2016-02-21 23:57:41,821 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+
+Success! You should now have a ``tutorial.sqlite`` file in your current
+working directory. This is an SQLite database with a single table defined in it
(``models``).
.. _wiki2-start-the-application:
Start the application
-=====================
+---------------------
Start the application.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/pserve development.ini --reload
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload
@@ -374,40 +386,57 @@ On Windows
If successful, you will see something like this on your console::
- Starting subprocess with file monitor
- Starting server in PID 8966.
- Starting HTTP server on http://0.0.0.0:6543
+ Starting subprocess with file monitor
+ Starting server in PID 82349.
+ serving on http://127.0.0.1:6543
This means the server is ready to accept requests.
+
Visit the application in a browser
-==================================
+----------------------------------
-In a browser, visit `http://localhost:6543/ <http://localhost:6543>`_. You
-will see the generated application's default page.
+In a browser, visit http://localhost:6543/. You will see the generated
+application's default page.
One thing you'll notice is the "debug toolbar" icon on right hand side of the
page. You can read more about the purpose of the icon at
:ref:`debug_toolbar`. It allows you to get information about your
application while you develop.
+
Decisions the ``alchemy`` scaffold has made for you
-===================================================
+---------------------------------------------------
Creating a project using the ``alchemy`` scaffold makes the following
assumptions:
-- you are willing to use :term:`SQLAlchemy` as a database access tool
+- You are willing to use :term:`SQLAlchemy` as a database access tool.
+
+- You are willing to use :term:`URL dispatch` to map URLs to code.
-- you are willing to use :term:`URL dispatch` to map URLs to code
+- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package to
+ scope sessions to requests.
-- you want to use ``ZopeTransactionExtension`` and ``pyramid_tm`` to scope
- sessions to requests
+- You want to use pyramid_jinja2_ to render your templates. Different
+ templating engines can be used, but we had to choose one to make this
+ tutorial. See :ref:`available_template_system_bindings` for some options.
.. note::
:app:`Pyramid` supports any persistent storage mechanism (e.g., object
- database or filesystem files). 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.
+ database or filesystem files). 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.
+
+.. _pyramid_jinja2:
+ http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/
+
+.. _pyramid_tm:
+ http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/
+
+.. _zope.sqlalchemy:
+ https://pypi.python.org/pypi/zope.sqlalchemy
+
+.. _transaction:
+ http://zodb.readthedocs.org/en/latest/transactions.html
diff --git a/docs/tutorials/wiki2/src/authentication/CHANGES.txt b/docs/tutorials/wiki2/src/authentication/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki2/src/authentication/MANIFEST.in b/docs/tutorials/wiki2/src/authentication/MANIFEST.in
new file mode 100644
index 000000000..42cd299b5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt
new file mode 100644
index 000000000..68f430110
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/README.txt
@@ -0,0 +1,14 @@
+tutorial README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/python setup.py develop
+
+- $VENV/bin/initialize_tutorial_db development.ini
+
+- $VENV/bin/pserve development.ini
+
diff --git a/docs/tutorials/wiki2/src/authentication/development.ini b/docs/tutorials/wiki2/src/authentication/development.ini
new file mode 100644
index 000000000..f3079727e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/development.ini
@@ -0,0 +1,73 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+ pyramid_tm
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+auth.secret = seekrit
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+host = 127.0.0.1
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_tutorial]
+level = DEBUG
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = INFO
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authentication/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini
new file mode 100644
index 000000000..686dba48a
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/production.ini
@@ -0,0 +1,62 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+auth.secret = real-seekrit
+
+[server:main]
+use = egg:waitress#main
+host = 0.0.0.0
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_tutorial]
+level = WARN
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py
new file mode 100644
index 000000000..57538f2d0
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/setup.py
@@ -0,0 +1,54 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.txt')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'bcrypt',
+ 'docutils',
+ 'pyramid',
+ 'pyramid_jinja2',
+ 'pyramid_debugtoolbar',
+ 'pyramid_tm',
+ 'SQLAlchemy',
+ 'transaction',
+ 'zope.sqlalchemy',
+ 'waitress',
+ ]
+
+tests_require = [
+ 'WebTest',
+]
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg pylons pyramid',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ test_suite='tutorial',
+ tests_require=tests_require,
+ install_requires=requires,
+ entry_points="""\
+ [paste.app_factory]
+ main = tutorial:main
+ [console_scripts]
+ initialize_tutorial_db = tutorial.scripts.initializedb:main
+ """,
+ )
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py
new file mode 100644
index 000000000..f5c033b8b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py
@@ -0,0 +1,13 @@
+from pyramid.config import Configurator
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.include('.security')
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/routes.py b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py
new file mode 100644
index 000000000..cb747244f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py
@@ -0,0 +1,8 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('login', '/login')
+ config.add_route('logout', '/logout')
+ config.add_route('view_page', '/{pagename}')
+ config.add_route('add_page', '/add_page/{pagename}')
+ config.add_route('edit_page', '/{pagename}/edit_page')
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py
new file mode 100644
index 000000000..5bb534f79
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py
new file mode 100644
index 000000000..f3c0a6fef
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py
@@ -0,0 +1,57 @@
+import os
+import sys
+import transaction
+
+from pyramid.paster import (
+ get_appsettings,
+ setup_logging,
+ )
+
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+from ..models import Page, User
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri> [var=value]\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) < 2:
+ usage(argv)
+ config_uri = argv[1]
+ options = parse_vars(argv[2:])
+ setup_logging(config_uri)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
+ Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py
new file mode 100644
index 000000000..8ea3858d2
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py
@@ -0,0 +1,27 @@
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+
+from .models import User
+
+
+class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ user = request.user
+ if user is not None:
+ return user.id
+
+def get_user(request):
+ user_id = request.unauthenticated_userid
+ if user_id is not None:
+ user = request.dbsession.query(User).get(user_id)
+ return user
+
+def includeme(config):
+ settings = config.get_settings()
+ authn_policy = MyAuthenticationPolicy(
+ settings['auth.secret'],
+ hashalg='sha512',
+ )
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(ACLAuthorizationPolicy())
+ config.add_request_method(get_user, 'user', reify=True)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png
new file mode 100644
index 000000000..4ab837be9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css
@@ -0,0 +1,154 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+body {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #ffffff;
+ background: #bc2131;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+}
+p {
+ font-weight: 300;
+}
+.font-normal {
+ font-weight: 400;
+}
+.font-semi-bold {
+ font-weight: 600;
+}
+.font-bold {
+ font-weight: 700;
+}
+.starter-template {
+ margin-top: 250px;
+}
+.starter-template .content {
+ margin-left: 10px;
+}
+.starter-template .content h1 {
+ margin-top: 10px;
+ font-size: 60px;
+}
+.starter-template .content h1 .smaller {
+ font-size: 40px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead {
+ font-size: 25px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead .font-normal {
+ color: #ffffff;
+}
+.starter-template .links {
+ float: right;
+ right: 0;
+ margin-top: 125px;
+}
+.starter-template .links ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+}
+.starter-template .links ul li {
+ list-style: none;
+ display: inline;
+ margin: 0 10px;
+}
+.starter-template .links ul li:first-child {
+ margin-left: 0;
+}
+.starter-template .links ul li:last-child {
+ margin-right: 0;
+}
+.starter-template .links ul li.current-version {
+ color: #f2b7bd;
+ font-weight: 400;
+}
+.starter-template .links ul li a, a {
+ color: #f2b7bd;
+ text-decoration: underline;
+}
+.starter-template .links ul li a:hover, a:hover {
+ color: #ffffff;
+ text-decoration: underline;
+}
+.starter-template .links ul li .icon-muted {
+ color: #eb8b95;
+ margin-right: 5px;
+}
+.starter-template .links ul li:hover .icon-muted {
+ color: #ffffff;
+}
+.starter-template .copyright {
+ margin-top: 10px;
+ font-size: 0.9em;
+ color: #f2b7bd;
+ text-transform: lowercase;
+ float: right;
+ right: 0;
+}
+@media (max-width: 1199px) {
+ .starter-template .content h1 {
+ font-size: 45px;
+ }
+ .starter-template .content h1 .smaller {
+ font-size: 30px;
+ }
+ .starter-template .content .lead {
+ font-size: 20px;
+ }
+}
+@media (max-width: 991px) {
+ .starter-template {
+ margin-top: 0;
+ }
+ .starter-template .logo {
+ margin: 40px auto;
+ }
+ .starter-template .content {
+ margin-left: 0;
+ text-align: center;
+ }
+ .starter-template .content h1 {
+ margin-bottom: 20px;
+ }
+ .starter-template .links {
+ float: none;
+ text-align: center;
+ margin-top: 60px;
+ }
+ .starter-template .copyright {
+ float: none;
+ text-align: center;
+ }
+}
+@media (max-width: 767px) {
+ .starter-template .content h1 .smaller {
+ font-size: 25px;
+ display: block;
+ }
+ .starter-template .content .lead {
+ font-size: 16px;
+ }
+ .starter-template .links {
+ margin-top: 40px;
+ }
+ .starter-template .links ul li {
+ display: block;
+ margin: 0;
+ }
+ .starter-template .links ul li .icon-muted {
+ display: none;
+ }
+ .starter-template .copyright {
+ margin-top: 20px;
+ }
+}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css
new file mode 100644
index 000000000..0d25de5b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css
@@ -0,0 +1 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
index 0f564b16c..44d14304e 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
@@ -1,21 +1,20 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -23,31 +22,27 @@
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
+
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong><span tal:replace="page.name">
- Page Name Goes Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2
new file mode 100644
index 000000000..1806de0ff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2
@@ -0,0 +1,26 @@
+{% extends 'layout.jinja2' %}
+
+{% block title %}Login - {% endblock title %}
+
+{% block content %}
+<p>
+<strong>
+ Login
+</strong><br>
+{{ message }}
+</p>
+<form action="{{ url }}" method="post">
+<input type="hidden" name="next" value="{{ next_url }}">
+<div class="form-group">
+ <label for="login">Username</label>
+ <input type="text" name="login" value="{{ login }}">
+</div>
+<div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" name="password">
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/tests.py b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py
new file mode 100644
index 000000000..c54945c28
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py
@@ -0,0 +1,65 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models import Base
+ Base.metadata.create_all(self.engine)
+
+ def tearDown(self):
+ from .models.meta import Base
+
+ testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
+
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py
new file mode 100644
index 000000000..2b993b430
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py
@@ -0,0 +1,46 @@
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import (
+ remember,
+ forget,
+ )
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..models import User
+
+
+@view_config(route_name='login', renderer='../templates/login.jinja2')
+def login(request):
+ next_url = request.params.get('next', request.referrer)
+ if not next_url:
+ next_url = request.route_url('view_wiki')
+ message = ''
+ login = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ user = request.dbsession.query(User).filter_by(name=login).first()
+ if user is not None and user.check_password(password):
+ headers = remember(request, user.id)
+ return HTTPFound(location=next_url, headers=headers)
+ message = 'Failed login'
+
+ return dict(
+ message=message,
+ url=request.route_url('login'),
+ next_url=next_url,
+ login=login,
+ )
+
+@view_config(route_name='logout')
+def logout(request):
+ headers = forget(request)
+ next_url = request.route_url('view_wiki')
+ return HTTPFound(location=next_url, headers=headers)
+
+@forbidden_view_config()
+def forbidden_view(request):
+ next_url = request.route_url('login', _query={'next': request.url})
+ return HTTPFound(location=next_url)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
new file mode 100644
index 000000000..1b071434c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
@@ -0,0 +1,79 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import (
+ HTTPForbidden,
+ HTTPFound,
+ HTTPNotFound,
+ )
+
+from pyramid.view import view_config
+
+from ..models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2')
+def view_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound('No such page')
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=page.name)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2')
+def edit_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ user = request.user
+ if user is None or (user.role != 'editor' and page.creator != user):
+ raise HTTPForbidden
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2')
+def add_page(request):
+ user = request.user
+ if user is None or user.role not in ('editor', 'basic'):
+ raise HTTPForbidden
+ pagename = request.matchdict['pagename']
+ if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ next_url = request.route_url('edit_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = request.user
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/authorization/MANIFEST.in b/docs/tutorials/wiki2/src/authorization/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/authorization/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/authorization/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini
index a9d53b296..f3079727e 100644
--- a/docs/tutorials/wiki2/src/authorization/development.ini
+++ b/docs/tutorials/wiki2/src/authorization/development.ini
@@ -17,6 +17,8 @@ pyramid.includes =
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+auth.secret = seekrit
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
@@ -27,7 +29,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
@@ -68,4 +70,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini
index 4684d2f7a..686dba48a 100644
--- a/docs/tutorials/wiki2/src/authorization/production.ini
+++ b/docs/tutorials/wiki2/src/authorization/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,17 +11,20 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+auth.secret = real-seekrit
+
[server:main]
use = egg:waitress#main
host = 0.0.0.0
port = 6543
-# Begin logging configuration
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +59,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
-
-# End logging configuration
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py
index 09bd63d33..57538f2d0 100644
--- a/docs/tutorials/wiki2/src/authorization/setup.py
+++ b/docs/tutorials/wiki2/src/authorization/setup.py
@@ -9,17 +9,22 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
]
+tests_require = [
+ 'WebTest',
+]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
@@ -38,6 +43,7 @@ setup(name='tutorial',
include_package_data=True,
zip_safe=False,
test_suite='tutorial',
+ tests_require=tests_require,
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
index 2ada42171..f5c033b8b 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
@@ -1,37 +1,13 @@
from pyramid.config import Configurator
-from pyramid.authentication import AuthTktAuthenticationPolicy
-from pyramid.authorization import ACLAuthorizationPolicy
-
-from sqlalchemy import engine_from_config
-
-from tutorial.security import groupfinder
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
- authn_policy = AuthTktAuthenticationPolicy(
- 'sosecret', callback=groupfinder, hashalg='sha512')
- authz_policy = ACLAuthorizationPolicy()
- config = Configurator(settings=settings,
- root_factory='tutorial.models.RootFactory')
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(authz_policy)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('view_wiki', '/')
- config.add_route('login', '/login')
- config.add_route('logout', '/logout')
- config.add_route('view_page', '/{pagename}')
- config.add_route('add_page', '/add_page/{pagename}')
- config.add_route('edit_page', '/{pagename}/edit_page')
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.include('.security')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models.py b/docs/tutorials/wiki2/src/authorization/tutorial/models.py
deleted file mode 100644
index 4f7e1e024..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from pyramid.security import (
- Allow,
- Everyone,
- )
-
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- 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)
-
-
-class RootFactory(object):
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
- def __init__(self, request):
- pass
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
new file mode 100644
index 000000000..f0a8b7f96
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
@@ -0,0 +1,56 @@
+from pyramid.httpexceptions import (
+ HTTPNotFound,
+ HTTPFound,
+)
+from pyramid.security import (
+ Allow,
+ Everyone,
+)
+
+from .models import Page
+
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('login', '/login')
+ config.add_route('logout', '/logout')
+ config.add_route('view_page', '/{pagename}', factory=page_factory)
+ config.add_route('add_page', '/add_page/{pagename}',
+ factory=new_page_factory)
+ config.add_route('edit_page', '/{pagename}/edit_page',
+ factory=page_factory)
+
+def new_page_factory(request):
+ pagename = request.matchdict['pagename']
+ if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ next_url = request.route_url('edit_page', pagename=pagename)
+ raise HTTPFound(location=next_url)
+ return NewPage(pagename)
+
+class NewPage(object):
+ def __init__(self, pagename):
+ self.pagename = pagename
+
+ def __acl__(self):
+ return [
+ (Allow, 'role:editor', 'create'),
+ (Allow, 'role:basic', 'create'),
+ ]
+
+def page_factory(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound
+ return PageResource(page)
+
+class PageResource(object):
+ def __init__(self, page):
+ self.page = page
+
+ def __acl__(self):
+ return [
+ (Allow, Everyone, 'view'),
+ (Allow, 'role:editor', 'edit'),
+ (Allow, str(self.page.creator_id), 'edit'),
+ ]
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
index 23a5f13f4..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
@@ -2,36 +2,56 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- Page,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import Page, User
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
index d88c9c71f..25cff7b05 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
@@ -1,7 +1,40 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
-GROUPS = {'editor':['group:editors']}
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+)
-def groupfinder(userid, request):
- if userid in USERS:
- return GROUPS.get(userid, [])
+from .models import User
+
+
+class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ user = request.user
+ if user is not None:
+ return user.id
+
+ def effective_principals(self, request):
+ principals = [Everyone]
+ user = request.user
+ if user is not None:
+ principals.append(Authenticated)
+ principals.append(str(user.id))
+ principals.append('role:' + user.role)
+ return principals
+
+def get_user(request):
+ user_id = request.unauthenticated_userid
+ if user_id is not None:
+ user = request.dbsession.query(User).get(user_id)
+ return user
+
+def includeme(config):
+ settings = config.get_settings()
+ authn_policy = MyAuthenticationPolicy(
+ settings['auth.secret'],
+ hashalg='sha512',
+ )
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(ACLAuthorizationPolicy())
+ config.add_request_method(get_user, 'user', reify=True)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css
index 2f924bcc5..0d25de5b6 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css
@@ -1 +1 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt
deleted file mode 100644
index ed355434d..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <p tal:condition="logged_in" class="pull-right">
- <a href="${request.application_url}/logout">Logout</a>
- </p>
- <p>
- Editing <strong><span tal:replace="page.name">Page Name Goes
- Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
- <form action="${save_url}" method="post">
- <div class="form-group">
- <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea>
- </div>
- <div class="form-group">
- <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
- </div>
- </form>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
index 02cb8e73b..44d14304e 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
@@ -1,21 +1,20 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -23,34 +22,27 @@
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
+
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
- <p tal:condition="logged_in" class="pull-right">
- <a href="${request.application_url}/logout">Logout</a>
- </p>
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong><span tal:replace="page.name">
- Page Name Goes Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
new file mode 100644
index 000000000..1806de0ff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
@@ -0,0 +1,26 @@
+{% extends 'layout.jinja2' %}
+
+{% block title %}Login - {% endblock title %}
+
+{% block content %}
+<p>
+<strong>
+ Login
+</strong><br>
+{{ message }}
+</p>
+<form action="{{ url }}" method="post">
+<input type="hidden" name="next" value="{{ next_url }}">
+<div class="form-group">
+ <label for="login">Username</label>
+ <input type="text" name="login" value="{{ login }}">
+</div>
+<div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" name="password">
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt
deleted file mode 100644
index 4a938e9bb..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt
+++ /dev/null
@@ -1,74 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>Login - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <p>
- <strong>
- Login
- </strong><br>
- <span tal:replace="message"></span>
- </p>
- <form action="${url}" method="post">
- <input type="hidden" name="came_from" value="${came_from}">
- <div class="form-group">
- <label for="login">Username</label>
- <input type="text" name="login" value="${login}">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" name="password" value="${password}">
- </div>
- <div class="form-group">
- <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
- </div>
- </form>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt
deleted file mode 100644
index c9b0cec21..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,66 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>Alchemy Scaffold for The Pyramid Web Framework</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
-
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="links">
- <ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
- <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
- <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
- <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
- </ul>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
index 9f01d2da5..c54945c28 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
@@ -3,144 +3,63 @@ import transaction
from pyramid import testing
-def _initTestingDB():
- from sqlalchemy import create_engine
- from tutorial.models import (
- DBSession,
- Page,
- Base
- )
- engine = create_engine('sqlite://')
- Base.metadata.create_all(engine)
- DBSession.configure(bind=engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
- 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 dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- self.session = _initTestingDB()
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
- def _callFUT(self, request):
- from tutorial.views import view_wiki
- return view_wiki(request)
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
- def test_it(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/FrontPage')
+ self.session = get_tm_session(session_factory, transaction.manager)
-class ViewPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
+ def init_database(self):
+ from .models import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- 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(name='IDoExist', data='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 = testing.setUp()
+ from .models.meta import Base
- def tearDown(self):
- self.session.remove()
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
- 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 = testing.setUp()
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
- 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(name='abc', data='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(name='abc', data='hello')
- self.session.add(page)
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/abc')
- self.assertEqual(page.data, 'Hello yo!')
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
deleted file mode 100644
index e954d5a31..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import re
-from docutils.core import publish_parts
-
-from pyramid.httpexceptions import (
- HTTPFound,
- HTTPNotFound,
- )
-
-from pyramid.view import (
- view_config,
- forbidden_view_config,
- )
-
-from pyramid.security import (
- remember,
- forget,
- )
-
-from .security import USERS
-
-from .models import (
- DBSession,
- Page,
- )
-
-
-# regular expression used to find WikiWords
-wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-
-@view_config(route_name='view_wiki',
- permission='view')
-def view_wiki(request):
- return HTTPFound(location = request.route_url('view_page',
- pagename='FrontPage'))
-
-@view_config(route_name='view_page', renderer='templates/view.pt',
- permission='view')
-def view_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).first()
- if page is None:
- return HTTPNotFound('No such page')
-
- def check(match):
- word = match.group(1)
- exists = DBSession.query(Page).filter_by(name=word).all()
- if exists:
- view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, word)
- else:
- add_url = request.route_url('add_page', 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 = request.route_url('edit_page', pagename=pagename)
- return dict(page=page, content=content, edit_url=edit_url,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='add_page', renderer='templates/edit.pt',
- permission='edit')
-def add_page(request):
- pagename = request.matchdict['pagename']
- if 'form.submitted' in request.params:
- body = request.params['body']
- page = Page(name=pagename, data=body)
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- save_url = request.route_url('add_page', pagename=pagename)
- page = Page(name='', data='')
- return dict(page=page, save_url=save_url,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='edit_page', renderer='templates/edit.pt',
- permission='edit')
-def edit_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).one()
- if 'form.submitted' in request.params:
- page.data = request.params['body']
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- return dict(
- page=page,
- save_url=request.route_url('edit_page', pagename=pagename),
- logged_in=request.authenticated_userid
- )
-
-@view_config(route_name='login', renderer='templates/login.pt')
-@forbidden_view_config(renderer='templates/login.pt')
-def login(request):
- login_url = request.route_url('login')
- 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,
- )
-
-@view_config(route_name='logout')
-def logout(request):
- headers = forget(request)
- return HTTPFound(location = request.route_url('view_wiki'),
- headers = headers)
-
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py
new file mode 100644
index 000000000..2b993b430
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py
@@ -0,0 +1,46 @@
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import (
+ remember,
+ forget,
+ )
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..models import User
+
+
+@view_config(route_name='login', renderer='../templates/login.jinja2')
+def login(request):
+ next_url = request.params.get('next', request.referrer)
+ if not next_url:
+ next_url = request.route_url('view_wiki')
+ message = ''
+ login = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ user = request.dbsession.query(User).filter_by(name=login).first()
+ if user is not None and user.check_password(password):
+ headers = remember(request, user.id)
+ return HTTPFound(location=next_url, headers=headers)
+ message = 'Failed login'
+
+ return dict(
+ message=message,
+ url=request.route_url('login'),
+ next_url=next_url,
+ login=login,
+ )
+
+@view_config(route_name='logout')
+def logout(request):
+ headers = forget(request)
+ next_url = request.route_url('view_wiki')
+ return HTTPFound(location=next_url, headers=headers)
+
+@forbidden_view_config()
+def forbidden_view(request):
+ next_url = request.route_url('login', _query={'next': request.url})
+ return HTTPFound(location=next_url)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
new file mode 100644
index 000000000..9358993ea
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
@@ -0,0 +1,64 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+from ..models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2',
+ permission='view')
+def view_page(request):
+ page = request.context.page
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=page.name)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2',
+ permission='edit')
+def edit_page(request):
+ page = request.context.page
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2',
+ permission='create')
+def add_page(request):
+ pagename = request.context.pagename
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = request.user
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini
index a9d53b296..99c4ff0fe 100644
--- a/docs/tutorials/wiki2/src/basiclayout/development.ini
+++ b/docs/tutorials/wiki2/src/basiclayout/development.ini
@@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
@@ -68,4 +68,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini
index fa94c1b3e..cb1db3211 100644
--- a/docs/tutorials/wiki2/src/basiclayout/production.ini
+++ b/docs/tutorials/wiki2/src/basiclayout/production.ini
@@ -11,8 +11,6 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
@@ -59,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py
index 15e7e5923..7bc697730 100644
--- a/docs/tutorials/wiki2/src/basiclayout/setup.py
+++ b/docs/tutorials/wiki2/src/basiclayout/setup.py
@@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
requires = [
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
@@ -19,6 +19,10 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest',
+]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
@@ -37,6 +41,7 @@ setup(name='tutorial',
include_package_data=True,
zip_safe=False,
test_suite='tutorial',
+ tests_require=tests_require,
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
index 867049e4f..4dab44823 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
@@ -1,21 +1,12 @@
from pyramid.config import Configurator
-from sqlalchemy import engine_from_config
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
config = Configurator(settings=settings)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('home', '/')
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py
deleted file mode 100644
index 11ddccadb..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- Index,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- 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(Text, unique=True)
- value = Column(Integer)
-
-Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
new file mode 100644
index 000000000..48a957ecb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
@@ -0,0 +1,73 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .mymodel import MyModel # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py
new file mode 100644
index 000000000..d65a01a42
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py
@@ -0,0 +1,18 @@
+from sqlalchemy import (
+ Column,
+ Index,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class MyModel(Base):
+ __tablename__ = 'models'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text)
+ value = Column(Integer)
+
+
+Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py
new file mode 100644
index 000000000..25504ad4d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py
@@ -0,0 +1,3 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
index 66feb3008..7307ecc5c 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
@@ -2,36 +2,44 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- MyModel,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import MyModel
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
model = MyModel(name='one', value=1)
- DBSession.add(model)
+ dbsession.add(model)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css
index 2f924bcc5..0d25de5b6 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css
@@ -1 +1 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..1917f83c7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2
index c9b0cec21..ff624c65b 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2
@@ -1,12 +1,12 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
<title>Alchemy Scaffold for The Pyramid Web Framework</title>
@@ -14,7 +14,7 @@
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -29,19 +29,19 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7.dev0</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2
new file mode 100644
index 000000000..bb622bf5a
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7.dev0</span>.</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt
deleted file mode 100644
index c9b0cec21..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,66 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>Alchemy Scaffold for The Pyramid Web Framework</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
-
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="links">
- <ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
- <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
- <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
- <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
- </ul>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
index 57a775e0a..c54945c28 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
@@ -3,31 +3,63 @@ import transaction
from pyramid import testing
-from .models import DBSession
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
-class TestMyView(unittest.TestCase):
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- from sqlalchemy import create_engine
- engine = create_engine('sqlite://')
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
from .models import (
- Base,
- MyModel,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = MyModel(name='one', value=55)
- DBSession.add(model)
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- DBSession.remove()
+ from .models.meta import Base
+
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
- def test_it(self):
- from .views import my_view
- request = testing.DummyRequest()
- info = my_view(request)
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
self.assertEqual(info['one'].name, 'one')
self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
index 4cfcae4af..ad0c728d7 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
@@ -3,26 +3,25 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from .models import (
- DBSession,
- MyModel,
- )
+from ..models import MyModel
-@view_config(route_name='home', renderer='templates/mytemplate.pt')
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(MyModel)
+ one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
- return Response(conn_err_msg, content_type='text/plain', status_int=500)
+ return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'tutorial'}
-conn_err_msg = """\
+
+db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
1. You may need to run the "initialize_tutorial_db" script
- to initialize your database tables. Check your virtual
+ to initialize your database tables. Check your virtual
environment's "bin" directory for this script and try to run it.
2. Your database server may not be running. Check that the
@@ -32,4 +31,3 @@ might be caused by one of the following things:
After you fix the problem, please restart the Pyramid application to
try it again.
"""
-
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/models/MANIFEST.in b/docs/tutorials/wiki2/src/models/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/models/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/models/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini
index a9d53b296..99c4ff0fe 100644
--- a/docs/tutorials/wiki2/src/models/development.ini
+++ b/docs/tutorials/wiki2/src/models/development.ini
@@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
@@ -68,4 +68,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini
index 4684d2f7a..cb1db3211 100644
--- a/docs/tutorials/wiki2/src/models/production.ini
+++ b/docs/tutorials/wiki2/src/models/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,8 +11,6 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
@@ -16,7 +19,10 @@ use = egg:waitress#main
host = 0.0.0.0
port = 6543
-# Begin logging configuration
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
-
-# End logging configuration
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py
index 15e7e5923..bdc9ceed7 100644
--- a/docs/tutorials/wiki2/src/models/setup.py
+++ b/docs/tutorials/wiki2/src/models/setup.py
@@ -9,8 +9,9 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
@@ -19,6 +20,10 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest',
+]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
@@ -37,6 +42,7 @@ setup(name='tutorial',
include_package_data=True,
zip_safe=False,
test_suite='tutorial',
+ tests_require=tests_require,
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
index 867049e4f..4dab44823 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
@@ -1,21 +1,12 @@
from pyramid.config import Configurator
-from sqlalchemy import engine_from_config
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
config = Configurator(settings=settings)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('home', '/')
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models.py b/docs/tutorials/wiki2/src/models/tutorial/models.py
deleted file mode 100644
index f028c917a..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/models.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- 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)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/page.py b/docs/tutorials/wiki2/src/models/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/models/tutorial/routes.py b/docs/tutorials/wiki2/src/models/tutorial/routes.py
new file mode 100644
index 000000000..25504ad4d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/routes.py
@@ -0,0 +1,3 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
index 23a5f13f4..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
@@ -2,36 +2,56 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- Page,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import Page, User
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css
index 2f924bcc5..0d25de5b6 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css
+++ b/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css
@@ -1 +1 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..1917f83c7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2
index c9b0cec21..ff624c65b 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2
@@ -1,12 +1,12 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
<title>Alchemy Scaffold for The Pyramid Web Framework</title>
@@ -14,7 +14,7 @@
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -29,19 +29,19 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7.dev0</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2
new file mode 100644
index 000000000..bb622bf5a
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7.dev0</span>.</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py
index 57a775e0a..c54945c28 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py
@@ -3,31 +3,63 @@ import transaction
from pyramid import testing
-from .models import DBSession
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
-class TestMyView(unittest.TestCase):
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- from sqlalchemy import create_engine
- engine = create_engine('sqlite://')
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
from .models import (
- Base,
- MyModel,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = MyModel(name='one', value=55)
- DBSession.add(model)
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- DBSession.remove()
+ from .models.meta import Base
+
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
- def test_it(self):
- from .views import my_view
- request = testing.DummyRequest()
- info = my_view(request)
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
self.assertEqual(info['one'].name, 'one')
self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
index 4cfcae4af..ad0c728d7 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
@@ -3,26 +3,25 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from .models import (
- DBSession,
- MyModel,
- )
+from ..models import MyModel
-@view_config(route_name='home', renderer='templates/mytemplate.pt')
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(MyModel)
+ one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
- return Response(conn_err_msg, content_type='text/plain', status_int=500)
+ return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'tutorial'}
-conn_err_msg = """\
+
+db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
1. You may need to run the "initialize_tutorial_db" script
- to initialize your database tables. Check your virtual
+ to initialize your database tables. Check your virtual
environment's "bin" directory for this script and try to run it.
2. Your database server may not be running. Check that the
@@ -32,4 +31,3 @@ might be caused by one of the following things:
After you fix the problem, please restart the Pyramid application to
try it again.
"""
-
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/tests/MANIFEST.in b/docs/tutorials/wiki2/src/tests/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/tests/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/tests/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini
index a9d53b296..f3079727e 100644
--- a/docs/tutorials/wiki2/src/tests/development.ini
+++ b/docs/tutorials/wiki2/src/tests/development.ini
@@ -17,6 +17,8 @@ pyramid.includes =
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+auth.secret = seekrit
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
@@ -27,7 +29,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
@@ -68,4 +70,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini
index 4684d2f7a..686dba48a 100644
--- a/docs/tutorials/wiki2/src/tests/production.ini
+++ b/docs/tutorials/wiki2/src/tests/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,17 +11,20 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+auth.secret = real-seekrit
+
[server:main]
use = egg:waitress#main
host = 0.0.0.0
port = 6543
-# Begin logging configuration
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +59,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
-
-# End logging configuration
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py
index d8486e462..57538f2d0 100644
--- a/docs/tutorials/wiki2/src/tests/setup.py
+++ b/docs/tutorials/wiki2/src/tests/setup.py
@@ -9,18 +9,22 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
- 'WebTest', # add this
]
+tests_require = [
+ 'WebTest',
+]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
@@ -39,6 +43,7 @@ setup(name='tutorial',
include_package_data=True,
zip_safe=False,
test_suite='tutorial',
+ tests_require=tests_require,
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
index cee89184b..f5c033b8b 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
@@ -1,37 +1,13 @@
from pyramid.config import Configurator
-from pyramid.authentication import AuthTktAuthenticationPolicy
-from pyramid.authorization import ACLAuthorizationPolicy
-
-from sqlalchemy import engine_from_config
-
-from tutorial.security import groupfinder
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
- authn_policy = AuthTktAuthenticationPolicy(
- 'sosecret', callback=groupfinder, hashalg='sha512')
- authz_policy = ACLAuthorizationPolicy()
- config = Configurator(settings=settings,
- root_factory='tutorial.models.RootFactory')
- config.include('pyramid_chameleon')
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(authz_policy)
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('view_wiki', '/')
- config.add_route('login', '/login')
- config.add_route('logout', '/logout')
- config.add_route('view_page', '/{pagename}')
- config.add_route('add_page', '/add_page/{pagename}')
- config.add_route('edit_page', '/{pagename}/edit_page')
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.include('.security')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models.py b/docs/tutorials/wiki2/src/tests/tutorial/models.py
deleted file mode 100644
index 4f7e1e024..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/models.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from pyramid.security import (
- Allow,
- Everyone,
- )
-
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- 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)
-
-
-class RootFactory(object):
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
- def __init__(self, request):
- pass
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/page.py b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py
new file mode 100644
index 000000000..f0a8b7f96
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py
@@ -0,0 +1,56 @@
+from pyramid.httpexceptions import (
+ HTTPNotFound,
+ HTTPFound,
+)
+from pyramid.security import (
+ Allow,
+ Everyone,
+)
+
+from .models import Page
+
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('login', '/login')
+ config.add_route('logout', '/logout')
+ config.add_route('view_page', '/{pagename}', factory=page_factory)
+ config.add_route('add_page', '/add_page/{pagename}',
+ factory=new_page_factory)
+ config.add_route('edit_page', '/{pagename}/edit_page',
+ factory=page_factory)
+
+def new_page_factory(request):
+ pagename = request.matchdict['pagename']
+ if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ next_url = request.route_url('edit_page', pagename=pagename)
+ raise HTTPFound(location=next_url)
+ return NewPage(pagename)
+
+class NewPage(object):
+ def __init__(self, pagename):
+ self.pagename = pagename
+
+ def __acl__(self):
+ return [
+ (Allow, 'role:editor', 'create'),
+ (Allow, 'role:basic', 'create'),
+ ]
+
+def page_factory(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound
+ return PageResource(page)
+
+class PageResource(object):
+ def __init__(self, page):
+ self.page = page
+
+ def __acl__(self):
+ return [
+ (Allow, Everyone, 'view'),
+ (Allow, 'role:editor', 'edit'),
+ (Allow, str(self.page.creator_id), 'edit'),
+ ]
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
index 23a5f13f4..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
@@ -2,36 +2,56 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- Page,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import Page, User
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py
index d88c9c71f..25cff7b05 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/security.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py
@@ -1,7 +1,40 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
-GROUPS = {'editor':['group:editors']}
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+)
-def groupfinder(userid, request):
- if userid in USERS:
- return GROUPS.get(userid, [])
+from .models import User
+
+
+class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ user = request.user
+ if user is not None:
+ return user.id
+
+ def effective_principals(self, request):
+ principals = [Everyone]
+ user = request.user
+ if user is not None:
+ principals.append(Authenticated)
+ principals.append(str(user.id))
+ principals.append('role:' + user.role)
+ return principals
+
+def get_user(request):
+ user_id = request.unauthenticated_userid
+ if user_id is not None:
+ user = request.dbsession.query(User).get(user_id)
+ return user
+
+def includeme(config):
+ settings = config.get_settings()
+ authn_policy = MyAuthenticationPolicy(
+ settings['auth.secret'],
+ hashalg='sha512',
+ )
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(ACLAuthorizationPolicy())
+ config.add_request_method(get_user, 'user', reify=True)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css
index 2f924bcc5..0d25de5b6 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css
+++ b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css
@@ -1 +1 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt
deleted file mode 100644
index 50e55c850..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt
+++ /dev/null
@@ -1,74 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <p>
- Editing <strong><span tal:replace="page.name">Page Name Goes
- Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
- <p class="pull-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </p>
- <form action="${save_url}" method="post">
- <div class="form-group">
- <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea>
- </div>
- <div class="form-group">
- <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
- </div>
- </form>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
index c0c1b6c20..44d14304e 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
@@ -1,21 +1,20 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -23,31 +22,27 @@
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
+
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
- <p>
- Editing <strong><span tal:replace="page.name">Page Name Goes
- Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
- <form action="${save_url}" method="post">
- <div class="form-group">
- <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea>
- </div>
- <div class="form-group">
- <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
- </div>
- </form>
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2
new file mode 100644
index 000000000..1806de0ff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2
@@ -0,0 +1,26 @@
+{% extends 'layout.jinja2' %}
+
+{% block title %}Login - {% endblock title %}
+
+{% block content %}
+<p>
+<strong>
+ Login
+</strong><br>
+{{ message }}
+</p>
+<form action="{{ url }}" method="post">
+<input type="hidden" name="next" value="{{ next_url }}">
+<div class="form-group">
+ <label for="login">Username</label>
+ <input type="text" name="login" value="{{ login }}">
+</div>
+<div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" name="password">
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt
deleted file mode 100644
index 5f8e9b98c..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt
+++ /dev/null
@@ -1,54 +0,0 @@
-<!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" xml:lang="en"
- xmlns:tal="http://xml.zope.org/namespaces/tal">
-<head>
- <title>Login - Pyramid tutorial wiki (based on TurboGears
- 20-Minute Wiki)</title>
- <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
- <meta name="keywords" content="python web application" />
- <meta name="description" content="pyramid web application" />
- <link rel="shortcut icon"
- href="${request.static_url('tutorial:static/favicon.ico')}" />
- <link rel="stylesheet"
- href="${request.static_url('tutorial:static/pylons.css')}"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="${request.static_url('tutorial:static/ie6.css')}"
- type="text/css" media="screen" charset="utf-8" />
- <![endif]-->
-</head>
-<body>
- <div id="wrap">
- <div id="top-small">
- <div class="top-small align-center">
- <div>
- <img width="220" height="50" alt="pyramid"
- src="${request.static_url('tutorial:static/pyramid-small.png')}" />
- </div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-right">
- <div id="left" class="app-welcome align-left">
- <b>Login</b><br/>
- <span tal:replace="message"/>
- </div>
- <div id="right" class="app-welcome align-right"></div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <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>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py
deleted file mode 100644
index c50e05b6d..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/tests.py
+++ /dev/null
@@ -1,235 +0,0 @@
-import unittest
-import transaction
-
-from pyramid import testing
-
-
-def _initTestingDB():
- from sqlalchemy import create_engine
- from tutorial.models import (
- DBSession,
- Page,
- Base
- )
- engine = create_engine('sqlite://')
- Base.metadata.create_all(engine)
- DBSession.configure(bind=engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
- 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 = testing.setUp()
-
- def tearDown(self):
- testing.tearDown()
-
- def _callFUT(self, request):
- from tutorial.views import view_wiki
- return view_wiki(request)
-
- def test_it(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/FrontPage')
-
-
-class ViewPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
-
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- 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(name='IDoExist', data='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 = testing.setUp()
-
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- 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 = testing.setUp()
-
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- 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(name='abc', data='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(name='abc', data='hello')
- self.session.add(page)
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/abc')
- self.assertEqual(page.data, 'Hello yo!')
-
-
-class FunctionalTests(unittest.TestCase):
-
- viewer_login = '/login?login=viewer&password=viewer' \
- '&came_from=FrontPage&form.submitted=Login'
- viewer_wrong_login = '/login?login=viewer&password=incorrect' \
- '&came_from=FrontPage&form.submitted=Login'
- editor_login = '/login?login=editor&password=editor' \
- '&came_from=FrontPage&form.submitted=Login'
-
- def setUp(self):
- from tutorial import main
- settings = { 'sqlalchemy.url': 'sqlite://'}
- app = main({}, **settings)
- from webtest import TestApp
- self.testapp = TestApp(app)
- _initTestingDB()
-
- def tearDown(self):
- del self.testapp
- from tutorial.models import DBSession
- DBSession.remove()
-
- def test_root(self):
- res = self.testapp.get('/', status=302)
- self.assertEqual(res.location, 'http://localhost/FrontPage')
-
- def test_FrontPage(self):
- res = self.testapp.get('/FrontPage', status=200)
- self.assertTrue(b'FrontPage' in res.body)
-
- def test_unexisting_page(self):
- self.testapp.get('/SomePage', status=404)
-
- def test_successful_log_in(self):
- res = self.testapp.get(self.viewer_login, status=302)
- self.assertEqual(res.location, 'http://localhost/FrontPage')
-
- def test_failed_log_in(self):
- res = self.testapp.get(self.viewer_wrong_login, status=200)
- self.assertTrue(b'login' in res.body)
-
- def test_logout_link_present_when_logged_in(self):
- self.testapp.get(self.viewer_login, status=302)
- res = self.testapp.get('/FrontPage', status=200)
- self.assertTrue(b'Logout' in res.body)
-
- def test_logout_link_not_present_after_logged_out(self):
- self.testapp.get(self.viewer_login, status=302)
- self.testapp.get('/FrontPage', status=200)
- res = self.testapp.get('/logout', status=302)
- self.assertTrue(b'Logout' not in res.body)
-
- def test_anonymous_user_cannot_edit(self):
- res = self.testapp.get('/FrontPage/edit_page', status=200)
- self.assertTrue(b'Login' in res.body)
-
- def test_anonymous_user_cannot_add(self):
- res = self.testapp.get('/add_page/NewPage', status=200)
- self.assertTrue(b'Login' in res.body)
-
- def test_viewer_user_cannot_edit(self):
- self.testapp.get(self.viewer_login, status=302)
- res = self.testapp.get('/FrontPage/edit_page', status=200)
- self.assertTrue(b'Login' in res.body)
-
- def test_viewer_user_cannot_add(self):
- self.testapp.get(self.viewer_login, status=302)
- res = self.testapp.get('/add_page/NewPage', status=200)
- self.assertTrue(b'Login' in res.body)
-
- def test_editors_member_user_can_edit(self):
- self.testapp.get(self.editor_login, status=302)
- res = self.testapp.get('/FrontPage/edit_page', status=200)
- self.assertTrue(b'Editing' in res.body)
-
- def test_editors_member_user_can_add(self):
- self.testapp.get(self.editor_login, status=302)
- res = self.testapp.get('/add_page/NewPage', status=200)
- self.assertTrue(b'Editing' in res.body)
-
- def test_editors_member_user_can_view(self):
- self.testapp.get(self.editor_login, status=302)
- res = self.testapp.get('/FrontPage', status=200)
- self.assertTrue(b'FrontPage' in res.body)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
new file mode 100644
index 000000000..b2c6e0975
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
@@ -0,0 +1,122 @@
+import transaction
+import unittest
+from webtest import TestApp
+
+
+class FunctionalTests(unittest.TestCase):
+
+ basic_login = (
+ '/login?login=basic&password=basic'
+ '&next=FrontPage&form.submitted=Login')
+ basic_wrong_login = (
+ '/login?login=basic&password=incorrect'
+ '&next=FrontPage&form.submitted=Login')
+ editor_login = (
+ '/login?login=editor&password=editor'
+ '&next=FrontPage&form.submitted=Login')
+
+ @classmethod
+ def setUpClass(cls):
+ from tutorial.models.meta import Base
+ from tutorial.models import (
+ User,
+ Page,
+ get_tm_session,
+ )
+ from tutorial import main
+
+ settings = {
+ 'sqlalchemy.url': 'sqlite://',
+ 'auth.secret': 'seekrit',
+ }
+ app = main({}, **settings)
+ cls.testapp = TestApp(app)
+
+ session_factory = app.registry['dbsession_factory']
+ cls.engine = session_factory.kw['bind']
+ Base.metadata.create_all(bind=cls.engine)
+
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ page1 = Page(name='FrontPage', data='This is the front page')
+ page1.creator = editor
+ page2 = Page(name='BackPage', data='This is the back page')
+ page2.creator = basic
+ dbsession.add_all([basic, editor, page1, page2])
+
+ @classmethod
+ def tearDownClass(cls):
+ from tutorial.models.meta import Base
+ Base.metadata.drop_all(bind=cls.engine)
+
+ def test_root(self):
+ res = self.testapp.get('/', status=302)
+ self.assertEqual(res.location, 'http://localhost/FrontPage')
+
+ def test_FrontPage(self):
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue(b'FrontPage' in res.body)
+
+ def test_unexisting_page(self):
+ self.testapp.get('/SomePage', status=404)
+
+ def test_successful_log_in(self):
+ res = self.testapp.get(self.basic_login, status=302)
+ self.assertEqual(res.location, 'http://localhost/FrontPage')
+
+ def test_failed_log_in(self):
+ res = self.testapp.get(self.basic_wrong_login, status=200)
+ self.assertTrue(b'login' in res.body)
+
+ def test_logout_link_present_when_logged_in(self):
+ self.testapp.get(self.basic_login, status=302)
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue(b'Logout' in res.body)
+
+ def test_logout_link_not_present_after_logged_out(self):
+ self.testapp.get(self.basic_login, status=302)
+ self.testapp.get('/FrontPage', status=200)
+ res = self.testapp.get('/logout', status=302)
+ self.assertTrue(b'Logout' not in res.body)
+
+ def test_anonymous_user_cannot_edit(self):
+ res = self.testapp.get('/FrontPage/edit_page', status=302).follow()
+ self.assertTrue(b'Login' in res.body)
+
+ def test_anonymous_user_cannot_add(self):
+ res = self.testapp.get('/add_page/NewPage', status=302).follow()
+ self.assertTrue(b'Login' in res.body)
+
+ def test_basic_user_cannot_edit_front(self):
+ self.testapp.get(self.basic_login, status=302)
+ res = self.testapp.get('/FrontPage/edit_page', status=302).follow()
+ self.assertTrue(b'Login' in res.body)
+
+ def test_basic_user_can_edit_back(self):
+ self.testapp.get(self.basic_login, status=302)
+ res = self.testapp.get('/BackPage/edit_page', status=200)
+ self.assertTrue(b'Editing' in res.body)
+
+ def test_basic_user_can_add(self):
+ self.testapp.get(self.basic_login, status=302)
+ res = self.testapp.get('/add_page/NewPage', status=200)
+ self.assertTrue(b'Editing' in res.body)
+
+ def test_editors_member_user_can_edit(self):
+ self.testapp.get(self.editor_login, status=302)
+ res = self.testapp.get('/FrontPage/edit_page', status=200)
+ self.assertTrue(b'Editing' in res.body)
+
+ def test_editors_member_user_can_add(self):
+ self.testapp.get(self.editor_login, status=302)
+ res = self.testapp.get('/add_page/NewPage', status=200)
+ self.assertTrue(b'Editing' in res.body)
+
+ def test_editors_member_user_can_view(self):
+ self.testapp.get(self.editor_login, status=302)
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue(b'FrontPage' in res.body)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py
new file mode 100644
index 000000000..2c945ab33
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py
@@ -0,0 +1,168 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
+ def setUp(self):
+ from ..models import get_tm_session
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('..models')
+ self.config.include('..routes')
+
+ session_factory = self.config.registry['dbsession_factory']
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ self.init_database()
+
+ def init_database(self):
+ from ..models.meta import Base
+ session_factory = self.config.registry['dbsession_factory']
+ engine = session_factory.kw['bind']
+ Base.metadata.create_all(engine)
+
+ def tearDown(self):
+ testing.tearDown()
+ transaction.abort()
+
+ def makeUser(self, name, role, password='dummy'):
+ from ..models import User
+ user = User(name=name, role=role)
+ user.set_password(password)
+ return user
+
+ def makePage(self, name, data, creator):
+ from ..models import Page
+ return Page(name=name, data=data, creator=creator)
+
+
+class ViewWikiTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+ self.config.include('..routes')
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, request):
+ from tutorial.views.default import view_wiki
+ return view_wiki(request)
+
+ def test_it(self):
+ request = testing.DummyRequest()
+ response = self._callFUT(request)
+ self.assertEqual(response.location, 'http://example.com/FrontPage')
+
+
+class ViewPageTests(BaseTest):
+ def _callFUT(self, request):
+ from tutorial.views.default import view_page
+ return view_page(request)
+
+ def test_it(self):
+ from ..routes import PageResource
+
+ # add a page to the db
+ user = self.makeUser('foo', 'editor')
+ page = self.makePage('IDoExist', 'Hello CruelWorld IDoExist', user)
+ self.session.add_all([page, user])
+
+ # create a request asking for the page we've created
+ request = dummy_request(self.session)
+ request.context = PageResource(page)
+
+ # call the view we're testing and check its behavior
+ 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(BaseTest):
+ def _callFUT(self, request):
+ from tutorial.views.default import add_page
+ return add_page(request)
+
+ def test_it_pageexists(self):
+ from ..models import Page
+ from ..routes import NewPage
+ request = testing.DummyRequest({'form.submitted': True,
+ 'body': 'Hello yo!'},
+ dbsession=self.session)
+ request.user = self.makeUser('foo', 'editor')
+ request.context = NewPage('AnotherPage')
+ self._callFUT(request)
+ pagecount = self.session.query(Page).filter_by(name='AnotherPage').count()
+ self.assertGreater(pagecount, 0)
+
+ def test_it_notsubmitted(self):
+ from ..routes import NewPage
+ request = dummy_request(self.session)
+ request.user = self.makeUser('foo', 'editor')
+ request.context = NewPage('AnotherPage')
+ info = self._callFUT(request)
+ self.assertEqual(info['pagedata'], '')
+ self.assertEqual(info['save_url'],
+ 'http://example.com/add_page/AnotherPage')
+
+ def test_it_submitted(self):
+ from ..models import Page
+ from ..routes import NewPage
+ request = testing.DummyRequest({'form.submitted': True,
+ 'body': 'Hello yo!'},
+ dbsession=self.session)
+ request.user = self.makeUser('foo', 'editor')
+ request.context = NewPage('AnotherPage')
+ self._callFUT(request)
+ page = self.session.query(Page).filter_by(name='AnotherPage').one()
+ self.assertEqual(page.data, 'Hello yo!')
+
+
+class EditPageTests(BaseTest):
+ def _callFUT(self, request):
+ from tutorial.views.default import edit_page
+ return edit_page(request)
+
+ def makeContext(self, page):
+ from ..routes import PageResource
+ return PageResource(page)
+
+ def test_it_notsubmitted(self):
+ user = self.makeUser('foo', 'editor')
+ page = self.makePage('abc', 'hello', user)
+ self.session.add_all([page, user])
+
+ request = dummy_request(self.session)
+ request.context = self.makeContext(page)
+ info = self._callFUT(request)
+ self.assertEqual(info['pagename'], 'abc')
+ self.assertEqual(info['save_url'],
+ 'http://example.com/abc/edit_page')
+
+ def test_it_submitted(self):
+ user = self.makeUser('foo', 'editor')
+ page = self.makePage('abc', 'hello', user)
+ self.session.add_all([page, user])
+
+ request = testing.DummyRequest({'form.submitted': True,
+ 'body': 'Hello yo!'},
+ dbsession=self.session)
+ request.context = self.makeContext(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/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py
deleted file mode 100644
index 41bea4785..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/views.py
+++ /dev/null
@@ -1,123 +0,0 @@
-import re
-from docutils.core import publish_parts
-
-from pyramid.httpexceptions import (
- HTTPFound,
- HTTPNotFound,
- )
-
-from pyramid.view import (
- view_config,
- forbidden_view_config,
- )
-
-from pyramid.security import (
- remember,
- forget,
- )
-
-from .security import USERS
-
-from .models import (
- DBSession,
- Page,
- )
-
-
-# regular expression used to find WikiWords
-wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-
-@view_config(route_name='view_wiki',
- permission='view')
-def view_wiki(request):
- return HTTPFound(location = request.route_url('view_page',
- pagename='FrontPage'))
-
-@view_config(route_name='view_page', renderer='templates/view.pt',
- permission='view')
-def view_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).first()
- if page is None:
- return HTTPNotFound('No such page')
-
- def check(match):
- word = match.group(1)
- exists = DBSession.query(Page).filter_by(name=word).all()
- if exists:
- view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, word)
- else:
- add_url = request.route_url('add_page', 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 = request.route_url('edit_page', pagename=pagename)
- return dict(page=page, content=content, edit_url=edit_url,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='add_page', renderer='templates/edit.pt',
- permission='edit')
-def add_page(request):
- pagename = request.matchdict['pagename']
- if 'form.submitted' in request.params:
- body = request.params['body']
- page = Page(name=pagename, data=body)
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- save_url = request.route_url('add_page', pagename=pagename)
- page = Page(name='', data='')
- return dict(page=page, save_url=save_url,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='edit_page', renderer='templates/edit.pt',
- permission='edit')
-def edit_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).one()
- if 'form.submitted' in request.params:
- page.data = request.params['body']
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- return dict(
- page=page,
- save_url=request.route_url('edit_page', pagename=pagename),
- logged_in=request.authenticated_userid
- )
-
-@view_config(route_name='login', renderer='templates/login.pt')
-@forbidden_view_config(renderer='templates/login.pt')
-def login(request):
- login_url = request.route_url('login')
- 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,
- )
-
-@view_config(route_name='logout')
-def logout(request):
- headers = forget(request)
- return HTTPFound(location = request.route_url('view_wiki'),
- headers = headers)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py
new file mode 100644
index 000000000..2b993b430
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py
@@ -0,0 +1,46 @@
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import (
+ remember,
+ forget,
+ )
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..models import User
+
+
+@view_config(route_name='login', renderer='../templates/login.jinja2')
+def login(request):
+ next_url = request.params.get('next', request.referrer)
+ if not next_url:
+ next_url = request.route_url('view_wiki')
+ message = ''
+ login = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ user = request.dbsession.query(User).filter_by(name=login).first()
+ if user is not None and user.check_password(password):
+ headers = remember(request, user.id)
+ return HTTPFound(location=next_url, headers=headers)
+ message = 'Failed login'
+
+ return dict(
+ message=message,
+ url=request.route_url('login'),
+ next_url=next_url,
+ login=login,
+ )
+
+@view_config(route_name='logout')
+def logout(request):
+ headers = forget(request)
+ next_url = request.route_url('view_wiki')
+ return HTTPFound(location=next_url, headers=headers)
+
+@forbidden_view_config()
+def forbidden_view(request):
+ next_url = request.route_url('login', _query={'next': request.url})
+ return HTTPFound(location=next_url)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
new file mode 100644
index 000000000..9358993ea
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
@@ -0,0 +1,64 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+from ..models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2',
+ permission='view')
+def view_page(request):
+ page = request.context.page
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=page.name)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2',
+ permission='edit')
+def edit_page(request):
+ page = request.context.page
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2',
+ permission='create')
+def add_page(request):
+ pagename = request.context.pagename
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = request.user
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/views/MANIFEST.in b/docs/tutorials/wiki2/src/views/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/views/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/views/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini
index a9d53b296..99c4ff0fe 100644
--- a/docs/tutorials/wiki2/src/views/development.ini
+++ b/docs/tutorials/wiki2/src/views/development.ini
@@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
@@ -68,4 +68,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini
index 4684d2f7a..cb1db3211 100644
--- a/docs/tutorials/wiki2/src/views/production.ini
+++ b/docs/tutorials/wiki2/src/views/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,8 +11,6 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
@@ -16,7 +19,10 @@ use = egg:waitress#main
host = 0.0.0.0
port = 6543
-# Begin logging configuration
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
-
-# End logging configuration
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py
index 09bd63d33..57538f2d0 100644
--- a/docs/tutorials/wiki2/src/views/setup.py
+++ b/docs/tutorials/wiki2/src/views/setup.py
@@ -9,17 +9,22 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
]
+tests_require = [
+ 'WebTest',
+]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
@@ -38,6 +43,7 @@ setup(name='tutorial',
include_package_data=True,
zip_safe=False,
test_suite='tutorial',
+ tests_require=tests_require,
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
index 37cae1997..4dab44823 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
@@ -1,24 +1,12 @@
from pyramid.config import Configurator
-from sqlalchemy import engine_from_config
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
config = Configurator(settings=settings)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('view_wiki', '/')
- config.add_route('view_page', '/{pagename}')
- config.add_route('add_page', '/add_page/{pagename}')
- config.add_route('edit_page', '/{pagename}/edit_page')
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models.py b/docs/tutorials/wiki2/src/views/tutorial/models.py
deleted file mode 100644
index f028c917a..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/models.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- 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)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/page.py b/docs/tutorials/wiki2/src/views/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/views/tutorial/routes.py b/docs/tutorials/wiki2/src/views/tutorial/routes.py
new file mode 100644
index 000000000..72df58efe
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/routes.py
@@ -0,0 +1,6 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('view_page', '/{pagename}')
+ config.add_route('add_page', '/add_page/{pagename}')
+ config.add_route('edit_page', '/{pagename}/edit_page')
diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
index 23a5f13f4..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
@@ -2,36 +2,56 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- Page,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import Page, User
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
index 4e5772de0..71785157f 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
@@ -1,21 +1,20 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -23,36 +22,18 @@
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
+
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong><span tal:replace="page.name">
- Page Name Goes Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
- <p class="pull-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </p>
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt
deleted file mode 100644
index c9b0cec21..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,66 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>Alchemy Scaffold for The Pyramid Web Framework</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
-
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="links">
- <ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
- <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
- <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
- <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
- </ul>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py
index 9f01d2da5..c54945c28 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py
@@ -3,144 +3,63 @@ import transaction
from pyramid import testing
-def _initTestingDB():
- from sqlalchemy import create_engine
- from tutorial.models import (
- DBSession,
- Page,
- Base
- )
- engine = create_engine('sqlite://')
- Base.metadata.create_all(engine)
- DBSession.configure(bind=engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
- 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 dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- self.session = _initTestingDB()
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
- def _callFUT(self, request):
- from tutorial.views import view_wiki
- return view_wiki(request)
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
- def test_it(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/FrontPage')
+ self.session = get_tm_session(session_factory, transaction.manager)
-class ViewPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
+ def init_database(self):
+ from .models import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- 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(name='IDoExist', data='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 = testing.setUp()
+ from .models.meta import Base
- def tearDown(self):
- self.session.remove()
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
- 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 = testing.setUp()
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
- 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(name='abc', data='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(name='abc', data='hello')
- self.session.add(page)
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/abc')
- self.assertEqual(page.data, 'Hello yo!')
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py
deleted file mode 100644
index a3707dab5..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/views.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import cgi
-import re
-from docutils.core import publish_parts
-
-from pyramid.httpexceptions import (
- HTTPFound,
- HTTPNotFound,
- )
-
-from pyramid.view import view_config
-
-from .models import (
- DBSession,
- Page,
- )
-
-# regular expression used to find WikiWords
-wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-
-@view_config(route_name='view_wiki')
-def view_wiki(request):
- return HTTPFound(location = request.route_url('view_page',
- pagename='FrontPage'))
-
-@view_config(route_name='view_page', renderer='templates/view.pt')
-def view_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).first()
- if page is None:
- return HTTPNotFound('No such page')
-
- def check(match):
- word = match.group(1)
- exists = DBSession.query(Page).filter_by(name=word).all()
- if exists:
- view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
- else:
- add_url = request.route_url('add_page', pagename=word)
- return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
-
- content = publish_parts(page.data, writer_name='html')['html_body']
- content = wikiwords.sub(check, content)
- edit_url = request.route_url('edit_page', pagename=pagename)
- return dict(page=page, content=content, edit_url=edit_url)
-
-@view_config(route_name='add_page', renderer='templates/edit.pt')
-def add_page(request):
- pagename = request.matchdict['pagename']
- if 'form.submitted' in request.params:
- body = request.params['body']
- page = Page(name=pagename, data=body)
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- save_url = request.route_url('add_page', pagename=pagename)
- page = Page(name='', data='')
- return dict(page=page, save_url=save_url)
-
-@view_config(route_name='edit_page', renderer='templates/edit.pt')
-def edit_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).one()
- if 'form.submitted' in request.params:
- page.data = request.params['body']
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- return dict(
- page=page,
- save_url = request.route_url('edit_page', pagename=pagename),
- )
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
new file mode 100644
index 000000000..bb6300b75
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
@@ -0,0 +1,73 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import (
+ HTTPFound,
+ HTTPNotFound,
+ )
+
+from pyramid.view import view_config
+
+from ..models import Page, User
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2')
+def view_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound('No such page')
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=page.name)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2')
+def edit_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2')
+def add_page(request):
+ pagename = request.matchdict['pagename']
+ if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ next_url = request.route_url('edit_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = (
+ request.dbsession.query(User).filter_by(name='editor').one())
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst
index 9db95334a..54aea28c6 100644
--- a/docs/tutorials/wiki2/tests.rst
+++ b/docs/tutorials/wiki2/tests.rst
@@ -2,87 +2,91 @@
Adding Tests
============
-We will now add tests for the models and the views and a few functional tests
-in ``tests.py``. Tests ensure that an application works, and that it
-continues to work when changes are made in the future.
+We will now add tests for the models and views as well as a few functional
+tests in a new ``tests`` subpackage. Tests ensure that an application works,
+and that it continues to work when changes are made in the future.
-Test the models
-===============
+The file ``tests.py`` was generated as part of the ``alchemy`` scaffold, but it
+is a common practice to put tests into a ``tests`` subpackage, especially as
+projects grow in size and complexity. Each module in the test subpackage
+should contain tests for its corresponding module in our application. Each
+corresponding pair of modules should have the same names, except the test
+module should have the prefix ``test_``.
+
+Start by deleting ``tests.py``, then create a new directory to contain our new
+tests as well as a new empty file ``tests/__init__.py``.
+
+.. warning::
+
+ It is very important when refactoring a Python module into a package to be
+ sure to delete the cache files (``.pyc`` files or ``__pycache__`` folders)
+ sitting around! Python will prioritize the cache files before traversing
+ into folders, using the old code, and you will wonder why none of your
+ changes are working!
-To test the model class ``Page`` we'll add a new ``PageModelTests`` class to
-our ``tests.py`` file that was generated as part of the ``alchemy`` scaffold.
Test the views
==============
-We'll modify our ``tests.py`` file, adding tests for each view function we
-added previously. As a result, we'll *delete* the ``ViewTests`` class that
-the ``alchemy`` scaffold provided, and add four other test classes:
+We'll create a new ``tests/test_views.py`` file, adding a ``BaseTest`` class
+used as the base for other test classes. Next we'll add tests for each view
+function we previously added to our application. We'll add four test classes:
``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``.
These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page``
views.
+
Functional tests
================
-We'll test the whole application, covering security aspects that are not
-tested in the unit tests, like logging in, logging out, checking that
-the ``viewer`` user cannot add or edit pages, but the ``editor`` user
-can, and so on.
+We'll test the whole application, covering security aspects that are not tested
+in the unit tests, like logging in, logging out, checking that the ``basic``
+user cannot edit pages that it didn't create but the ``editor`` user can, and
+so on.
+
-View the results of all our edits to ``tests.py``
-=================================================
+View the results of all our edits to ``tests`` subpackage
+=========================================================
-Open the ``tutorial/tests.py`` module, and edit it such that it appears as
+Open ``tutorial/tests/test_views.py``, and edit it such that it appears as
follows:
-.. literalinclude:: src/tests/tutorial/tests.py
+.. literalinclude:: src/tests/tutorial/tests/test_views.py
:linenos:
:language: python
-Running the tests
-=================
-
-We can run these tests by using ``setup.py test`` in the same way we did in
-:ref:`running_tests`. However, first we must edit our ``setup.py`` to
-include a dependency on WebTest, which we've used in our ``tests.py``.
-Change the ``requires`` list in ``setup.py`` to include ``WebTest``.
+Open ``tutorial/tests/test_functional.py``, and edit it such that it appears as
+follows:
-.. literalinclude:: src/tests/setup.py
+.. literalinclude:: src/tests/tutorial/tests/test_functional.py
:linenos:
:language: python
- :lines: 11-22
- :emphasize-lines: 11
-After we've added a dependency on WebTest in ``setup.py``, we need to run
-``setup.py develop`` to get WebTest installed into our virtualenv. Assuming
-our shell's current working directory is the "tutorial" distribution
-directory:
-On UNIX:
+.. note::
-.. code-block:: text
+ We're utilizing the excellent WebTest_ package to do functional testing of
+ the application. This is defined in the ``tests_require`` section of our
+ ``setup.py``. Any other dependencies needed only for testing purposes can be
+ added there and will be installed automatically when running
+ ``setup.py test``.
- $ $VENV/bin/python setup.py develop
-On Windows:
-
-.. code-block:: text
-
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+Running the tests
+=================
-Once that command has completed successfully, we can run the tests
-themselves:
+We can run these tests by using ``setup.py test`` in the same way we did in
+:ref:`running_tests`:
On UNIX:
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/python setup.py test -q
On Windows:
-.. code-block:: text
+.. code-block:: ps1con
c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
@@ -90,8 +94,17 @@ The expected result should look like the following:
.. code-block:: text
- ......................
+ .....................
----------------------------------------------------------------------
- Ran 21 tests in 2.700s
+ Ran 22 tests in 5.320s
OK
+
+.. note:: If you use Python 3 during this tutorial, you will see deprecation
+ warnings in the output, which we will choose to ignore. In making this
+ tutorial run on both Python 2 and 3, the authors prioritized simplicity and
+ focus for the learner over accommodating warnings. In your own app or as
+ extra credit, you may choose to either drop Python 2 support or hack your
+ code to work without warnings on both Python 2 and 3.
+
+.. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/