summaryrefslogtreecommitdiff
path: root/docs/tutorials
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2016-02-18 02:39:43 -0600
committerMichael Merickel <michael@merickel.org>2016-02-18 02:39:43 -0600
commit5dc1c80046b7eb83fb7c51105bed0e73b2ab759c (patch)
treea66d8ee8f8282a72a5ada4fc5d903486caac6a85 /docs/tutorials
parent3eb1c354d320536ee470b79dcb930d20da93d97d (diff)
parent66fabb4ac707b5b4289db0094756f1a1af7269cc (diff)
downloadpyramid-5dc1c80046b7eb83fb7c51105bed0e73b2ab759c.tar.gz
pyramid-5dc1c80046b7eb83fb7c51105bed0e73b2ab759c.tar.bz2
pyramid-5dc1c80046b7eb83fb7c51105bed0e73b2ab759c.zip
Merge pull request #2334 from mmerickel/feature/alchemy-scaffold-update-tweaks
object-level security and tons of other small improvements to the wiki2 tutorial
Diffstat (limited to 'docs/tutorials')
-rw-r--r--docs/tutorials/wiki2/authentication.rst296
-rw-r--r--docs/tutorials/wiki2/authorization.rst445
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst334
-rw-r--r--docs/tutorials/wiki2/definingmodels.rst245
-rw-r--r--docs/tutorials/wiki2/definingviews.rst381
-rw-r--r--docs/tutorials/wiki2/design.rst94
-rw-r--r--docs/tutorials/wiki2/distributing.rst5
-rw-r--r--docs/tutorials/wiki2/index.rst1
-rw-r--r--docs/tutorials/wiki2/installation.rst32
-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__.py (renamed from docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py)0
-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.jinja264
-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.py76
-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.ini2
-rw-r--r--docs/tutorials/wiki2/src/authorization/production.ini4
-rw-r--r--docs/tutorials/wiki2/src/authorization/setup.py8
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/__init__.py22
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py75
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py33
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py26
-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.py50
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py39
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/security.py40
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/security/default.py7
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja293
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja228
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2100
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja28
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja289
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/tests.py18
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/default.py118
-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/production.ini2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/setup.py5
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py5
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py72
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py33
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py3
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py3
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 (renamed from docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2)2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py4
-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/production.ini2
-rw-r--r--docs/tutorials/wiki2/src/models/setup.py6
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/__init__.py5
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/__init__.py75
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/meta.py33
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py14
-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.py39
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 (renamed from docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2)2
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/tests.py18
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/default.py4
-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.ini2
-rw-r--r--docs/tutorials/wiki2/src/tests/production.ini4
-rw-r--r--docs/tutorials/wiki2/src/tests/setup.py9
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/__init__.py22
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py75
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/meta.py33
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py26
-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.py50
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py39
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/security.py40
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py1
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/security/default.py7
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja293
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja228
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2100
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja289
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py1
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py131
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py108
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/default.py118
-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/production.ini2
-rw-r--r--docs/tutorials/wiki2/src/views/setup.py8
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/__init__.py8
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/__init__.py75
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/meta.py33
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py14
-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.py39
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja288
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja219
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja284
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/tests.py18
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/default.py55
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/tests.rst51
135 files changed, 3583 insertions, 2147 deletions
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
new file mode 100644
index 000000000..1b18e5c55
--- /dev/null
+++ b/docs/tutorials/wiki2/authentication.rst
@@ -0,0 +1,296 @@
+.. _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/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 a :term:`authentication policy`
+which is used to identify authentication information from a ``request``,
+as well as handling the low-level login/logout operations required to
+track users across requests (via cookies or headers or whatever else you can
+imagine).
+
+Add the authentication policy
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create a new file ``tutorial/security.py``:
+
+.. literalinclude:: src/authentication/tutorial/security.py
+ :linenos:
+ :emphasize-lines: 9,14,21-27
+ :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.
+* A ``get_user`` function which can convert the ``unauthenticated_userid``
+ from the policy into a ``User`` object from our database.
+* 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.
+
+The logic in this file is a little bit interesting and 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
+
+Only the highlighted lines need to be changed.
+
+Now edit the ``add_page`` view function:
+
+.. literalinclude:: src/authentication/tutorial/views/default.py
+ :lines: 62-76
+ :lineno-match:
+ :emphasize-lines: 3-5,10
+ :language: python
+
+Only the highlighted lines need to be changed.
+
+If the user 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 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.
+
+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 is not logged in or the user is not the page's creator **and**
+not an ``editor`` then we raise ``HTTPForbidden``.
+
+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!
+
+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
+ :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`` where we will add the following
+code:
+
+.. literalinclude:: src/authentication/tutorial/views/auth.py
+ :linenos:
+ :language: python
+
+This code adds 3 new views to 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 and
+ then using our ``user.check_password`` method to compare the 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 the page 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` and 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 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 a "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 active only when the user is logged in and
+vice versa the login link is only active 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.
+
+- 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 the ``editor`` or ``basic`` user. If a different user
+ (or the anonymous user) invokes it, a login form will be displayed. Supplying
+ the credentials with the username ``basic``, password ``basic`` will display
+ the edit page form.
+
+- http://localhost:6543/SomePageName/edit_page is editable by the ``basic``
+ if the page was created by that user in the previous step. If, instead, the
+ page was created by ``editor`` 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.
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index e40433497..eb9269dff 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -4,378 +4,217 @@
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/default.py``, a new subpackage).
-* Add an :term:`ACL` (``models/mymodel.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/default.py``).
-
-Then we will add the login and logout feature:
-
-* Add routes for /login and /logout (``__init__.py``).
-* Add ``login`` and ``logout`` views (``views/default.py``).
-* Add a login template (``login.jinja2``).
-* Make the existing views return a ``logged_in`` flag to the renderer
+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 a "Logout" link to be shown when logged in and viewing or editing a page
- (``view.jinja2``, ``edit.jinja2``).
+Add user principals
+-------------------
-Access control
---------------
+A :term:`principal` is a level of abstraction on top of the raw
+:term:`userid` that describes the user in terms of 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.
-Add users and groups
-~~~~~~~~~~~~~~~~~~~~
+: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`` and ``basic``. These will be prefixed by the ``role:``
+string to avoid clasing with any other types of principals.
-Create a new ``tutorial/tutorial/security/default.py`` subpackage with the
-following content:
+Open the file ``tutorial/security.py`` and edit the following lines:
-.. literalinclude:: src/authorization/tutorial/security/default.py
+.. 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:
-
-- 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``.
-
-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.
+Only the highlighted lines need to be added.
-In a production system, user and group data will most often come from a
-database, but here we use "dummy" data to represent user and groups sources.
+Note that the role comes from the ``User`` object and finally we also
+add the ``user.id`` as a principal for when we want to allow that exact
+user to edit page's they've created.
-Add an ACL
-~~~~~~~~~~
+Add the authorization policy
+----------------------------
-Open ``tutorial/tutorial/models/mymodel.py`` and add the following import
-statement just after the ``Base`` import at the top:
+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 and so we'll
+mention it now.
-.. literalinclude:: src/authorization/tutorial/models/mymodel.py
- :lines: 3-6
- :linenos:
- :lineno-start: 3
- :language: python
+Open the file ``tutorial/security.py`` and notice the following lines:
-Add the following class definition at the end:
-
-.. literalinclude:: src/authorization/tutorial/models/mymodel.py
- :lines: 22-26
- :linenos:
- :lineno-start: 22
+.. literalinclude:: src/authorization/tutorial/security.py
+ :lines: 38-40
+ :lineno-match:
+ :emphasize-lines: 2
: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.
-
-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 (``Everyone``) the `view` permission. The second entry allows
-the ``group:editors`` principal the `edit` permission.
+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__``.
-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.
+Add resources and ACLs
+----------------------
-Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to
-our :term:`Configurator` constructor, that points to the class we created
-above:
+Resources are the hidden gem of :app:`Pyramid`. You've made it!
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 13-14
- :emphasize-lines: 2
- :language: python
+Every URL in a web application is representing 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.
-Only the highlighted line needs to be added.
+Our wiki has two resources:
-We are now providing the ACL to the application. See :ref:`assigning_acls`
-for more information about what an :term:`ACL` represents.
+#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited.
+ Only ``editor`` users as well as the original creator of the ``Page``
+ may edit the ``PageResource`` but anyone may view it.
-.. 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.
+#. A ``NewPage``. Represents a potential ``Page`` that does not exist.
+ Any logged-in user (roles ``basic`` or ``editor``) can create pages.
-Add authentication and authorization policies
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. note::
-Open ``tutorial/tutorial/__init__.py`` and add the highlighted import
-statements:
+ The wiki data model is simple enough that the ``PageResource`` is
+ actually 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 it clear the
+ parts that :app:`Pyramid` cares about versus application-defined objects.
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 1-5
- :linenos:
- :emphasize-lines: 2-5
- :language: python
+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!
-Now add those policies to the configuration:
+Open the file ``tutorial/routes.py`` and edit the following lines:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 7-16
+.. literalinclude:: src/authorization/tutorial/routes.py
:linenos:
- :lineno-start: 7
- :emphasize-lines: 4-6,9-10
+ :emphasize-lines: 1-7,14-50
:language: python
-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
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The highlighted lines need to be edited or added.
-Open ``tutorial/tutorial/views/default.py`` and add a ``permission='view'``
-parameter to the ``@view_config`` decorator for ``view_wiki()`` and
-``view_page()`` as follows:
+The ``NewPage`` 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 only
+allow users with the principals ``role:editor`` and ``role:basic`` to
+have the ``create`` permission:
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 27-29
- :emphasize-lines: 1-2
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 20-32
+ :lineno-match:
+ :emphasize-lines: 11,12
:language: python
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 33-35
- :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.
-
-Add a ``permission='edit'`` parameter to the ``@view_config`` decorators for
-``add_page()`` and ``edit_page()``:
-
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 57-59
- :emphasize-lines: 1-2
- :language: python
+The ``NewPage`` is loaded as the :term:`context` of the ``add_page``
+route by declaring a ``factory`` on the route:
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 72-74
- :emphasize-lines: 1-2
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 15-16
+ :lineno-match:
+ :emphasize-lines: 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.
-
-We are done with the changes needed to control access. The changes that
-follow will add the login and logout feature.
-
-Login, logout
--------------
+The ``PageResource`` defines the :term:`ACL` for a ``Page``. It uses an
+actual ``Page`` object to determine **who** can do **what** to the page.
-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: 20-23
- :emphasize-lines: 2-3
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 34-50
+ :lineno-match:
+ :emphasize-lines: 14-16
:language: python
-.. note:: The preceding lines must be added *before* the following
- ``view_page`` route definition:
-
- .. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 23
- :language: python
+The ``PageResource`` is loaded as the :term:`context` of the ``view_page``
+and ``edit_page`` route by declaring a ``factory`` on the routes:
- 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}``.
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 14-18
+ :lineno-match:
+ :emphasize-lines: 1,4-5
+ :language: python
-Add login and logout views
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+Add view permissions
+--------------------
-We'll add a ``login`` view which renders a login form and processes the post
-from the login form, checking credentials.
+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.
-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.
+Open the file ``views/default.py``.
-Add the following import statements to ``tutorial/tutorial/views/default.py``
-after the import from ``pyramid.httpexceptions``:
+First, you can drop a few imports that are no longer necessary:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 10-20
- :emphasize-lines: 1-11
+ :lines: 5-7
+ :lineno-match:
+ :emphasize-lines: 1
: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:
+Edit the ``view_page`` view to declare the ``view`` permission and remove
+the explicit checks within the view:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 88-121
+ :lines: 18-23
+ :lineno-match:
+ :emphasize-lines: 2,4
: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``, and
-- 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.
-
-The order of these two :term:`view configuration` decorators is unimportant.
-
-``logout()`` is decorated with a ``@view_config`` decorator which associates
-it with the ``logout`` route. It will be invoked when we visit ``/logout``.
-
-Add the ``login.jinja2`` template
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Create ``tutorial/tutorial/templates/login.jinja2`` with the following content:
-
-.. literalinclude:: src/authorization/tutorial/templates/login.jinja2
- :language: html
-
-The above template is referenced in the login view that we just added in
-``views/default.py``.
-
-Return a ``logged_in`` flag to the renderer
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+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 ``HTTPNotFound`` otherwise - again simplifying the view logic.
-Open ``tutorial/tutorial/views/default.py`` again. Add a ``logged_in``
-parameter to the return value of ``view_page()``, ``add_page()``, and
-``edit_page()`` as follows:
+Edit the ``edit_page`` view to declare the ``edit`` permission:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 54-55
- :emphasize-lines: 1-2
+ :lines: 38-42
+ :lineno-match:
+ :emphasize-lines: 2,4
:language: python
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 69-70
- :emphasize-lines: 1-2
- :language: python
+Edit the ``add_page`` view to declare the ``create`` permission:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 82-86
- :emphasize-lines: 3-4
+ :lines: 52-56
+ :lineno-match:
+ :emphasize-lines: 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
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Open ``tutorial/tutorial/templates/edit.jinja2`` and
-``tutorial/tutorial/templates/view.jinja2`` and add the following code as
-indicated by the highlighted lines.
+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/templates/edit.jinja2
- :lines: 34-40
- :emphasize-lines: 3-7
- :language: html
+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 a ``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.
-The attribute ``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,5,10-12,14-16,21-22
- :language: python
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/models/mymodel.py`` will look like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/models/mymodel.py
- :linenos:
- :emphasize-lines: 3-6,22-26
- :language: python
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/views/default.py`` will look like this when we're done:
+The final ``tutorial/views/default.py`` should look like the following:
.. literalinclude:: src/authorization/tutorial/views/default.py
:linenos:
- :emphasize-lines: 10-20,27-28,33-34,54-55,57-58,69-70,72-73,84-85,88-121
:language: python
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/templates/edit.jinja2`` template will look like this when
-we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2
- :linenos:
- :emphasize-lines: 36-40
- :language: html
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/templates/view.jinja2`` template will look like this when
-we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/view.jinja2
- :linenos:
- :emphasize-lines: 36-40
- :language: html
-
-Only the highlighted lines need to be added or edited.
-
Viewing the application in a browser
------------------------------------
diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst
index e3d0a0a3c..1ae51eb93 100644
--- a/docs/tutorials/wiki2/basiclayout.rst
+++ b/docs/tutorials/wiki2/basiclayout.rst
@@ -16,29 +16,30 @@ 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
+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:
- .. 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
- :lineno-start: 4
- :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`
@@ -46,10 +47,10 @@ application. (See :ref:`startup_chapter` for more about ``pserve``.)
Next in ``main``, construct a :term:`Configurator` object:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 7
- :lineno-start: 7
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 7
+ :lineno-match:
+ :language: py
``settings`` is passed to the Configurator as a keyword argument with the
dictionary values passed as the ``**settings`` argument. This will be a
@@ -60,48 +61,32 @@ deployment-related values such as ``pyramid.reload_templates``,
Next include :term:`Jinja2` templating bindings so that we can use renderers
with the ``.jinja2`` extension within our project.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 8
- :lineno-start: 8
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 8
+ :lineno-match:
+ :language: py
-Next include the module ``meta`` from the package ``models`` using a dotted
-Python path.
+Next include the the package ``models`` using a dotted Python path. The
+exact setup of the models will be covered later.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 9
- :lineno-start: 9
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 9
+ :lineno-match:
+ :language: py
-``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with
-two arguments: ``static`` (the name), and ``static`` (the path):
+Next include the ``routes`` module using a dotted Python path. This module
+will be explained in the next section.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 10
- :lineno-start: 10
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 10
+ :lineno-match:
+ :language: py
-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
-``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
-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 ``/``:
-
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 11
- :lineno-start: 11
- :language: py
+.. note::
-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/``.
+ 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
@@ -110,19 +95,49 @@ 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: 12
- :lineno-start: 12
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 11
+ :lineno-match:
+ :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: 13
- :lineno-start: 13
- :language: py
+.. 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
+
+First, on line 2, call :meth:`pyramid.config.Configurator.add_static_view`
+with two arguments: ``static`` (the name), and ``static`` (the path).
+
+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
+``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
+a static file resource, such as a CSS file.
+
+Second, 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/``.
View declarations via the ``views`` package
@@ -133,12 +148,12 @@ The main function of a web framework is mapping each URL pattern to code (a
corresponding :term:`route`. Our application uses the
:meth:`pyramid.view.view_config` decorator to perform this mapping.
-Open ``tutorial/tutorial/views/default.py`` in the ``views`` package. It
+Open ``tutorial/views/default.py`` in the ``views`` package. It
should already contain the following:
- .. literalinclude:: src/basiclayout/tutorial/views/default.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`,
@@ -178,92 +193,46 @@ In a 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.
-First, open ``tutorial/tutorial/models/__init__.py``, which should already
-contain the following:
-
- .. literalinclude:: src/basiclayout/tutorial/models/__init__.py
- :linenos:
- :language: py
-
-Our ``__init__.py`` will perform some imports to support later code, then calls
-the function :func:`sqlalchemy.orm.configure_mappers`.
-
-Next open ``tutorial/tutorial/models/meta.py``, which should already contain
+First, open ``tutorial/models/meta.py``, which should already contain
the following:
- .. literalinclude:: src/basiclayout/tutorial/models/meta.py
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :linenos:
+ :language: py
-``meta.py`` contains imports that are used to support later code. We create a
-dictionary ``NAMING_CONVENTION`` as well.
+``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.
- .. literalinclude:: src/basiclayout/tutorial/models/meta.py
- :end-before: metadata
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :end-before: metadata
+ :linenos:
+ :language: py
Next we create a ``metadata`` object from the class
:class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value
-for the ``naming_convention`` argument. We also need to create a declarative
-``Base`` object to use as a base class for our model. Then our model classes
-will inherit from the ``Base`` class so they can be associated with our
-particular database connection.
-
- .. literalinclude:: src/basiclayout/tutorial/models/meta.py
- :lines: 18-19
- :lineno-start: 18
- :linenos:
- :language: py
-
-Next we define several functions, the first of which is ``includeme``, which
-configures various database settings by calling subsequently defined functions.
-
- .. literalinclude:: src/basiclayout/tutorial/models/meta.py
- :pyobject: includeme
- :lineno-start: 22
- :linenos:
- :language: py
-
-The function ``get_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.
+for the ``naming_convention`` argument.
+
+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.
- .. literalinclude:: src/basiclayout/tutorial/models/meta.py
- :pyobject: get_session
- :lineno-start: 35
- :linenos:
- :language: py
-
-The ``get_engine`` function 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, which is a
-URI, something like ``sqlite://``.
-
- .. literalinclude:: src/basiclayout/tutorial/models/meta.py
- :pyobject: get_engine
- :lineno-start: 42
- :linenos:
- :language: py
-
-The function ``get_dbmaker`` accepts an :term:`SQLAlchemy` database engine,
-and creates a database session object ``dbmaker`` from the :term:`SQLAlchemy`
-class :class:`sqlalchemy.orm.session.sessionmaker`, which is then used for
-creating a session with the database engine.
-
- .. literalinclude:: src/basiclayout/tutorial/models/meta.py
- :pyobject: get_dbmaker
- :lineno-start: 46
- :linenos:
- :language: py
-
-To give a simple example of a model class, we define one named ``MyModel``:
-
- .. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
- :pyobject: MyModel
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :lines: 15-16
+ :lineno-match:
+ :linenos:
+ :language: py
+
+We've defined the ``models`` as a packge to make it straightforward to
+define models separately in different modules. To give a simple example of a
+model class, we define one named ``MyModel`` in a ``mymodel.py``:
+
+.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
+ :pyobject: MyModel
+ :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
@@ -279,8 +248,83 @@ The ``MyModel`` class has a ``__tablename__`` attribute. This informs
SQLAlchemy which table to use to store the data representing instances of this
class.
+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`` 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`` 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 is not required for this
-tutorial, and will be removed in the next step.
+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 b38177d04..41f36fa26 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -3,71 +3,148 @@ 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 ``mymodel.py`` file.
+be to define a wiki page :term:`domain model`.
+.. note::
-Edit ``mymodel.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 ``mymodel.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`` (as we've done in this tutorial), but this is only by convention.
+Declaring dependencies in our ``setup.py`` file
+===============================================
-Open the ``tutorial/tutorial/models/mymodel.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 on 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/tutorial/models/mymodel.py
+.. 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:: text
+
+ 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``
+---------------------
+
+The first thing we'll do is 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/user.py
:linenos:
:language: py
- :emphasize-lines: 9-11,13,14
-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 a SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This
+will attach the model to our schema.
-Then we added a ``Page`` class. Because this is an 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/mymodel.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. The
+``check_password`` method will allow us to compare input passwords to
+see if they resolve to the same hash signifying a match.
+
+
+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 the ``creator`` attribute which is 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.
Edit ``models/__init__.py``
---------------------------
Since we are using a package for our models, we also need to update our
-``__init__.py`` file.
+``__init__.py`` file to ensure that the models are attached to the metadata.
-Open the ``tutorial/tutorial/models/__init__.py`` file and edit it to look like
+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: 4
+ :emphasize-lines: 8,9
-Here we need to align our import with the name of the model ``Page``.
+Here we need to align our imports with the names of the models ``User``,
+and ``Page``.
Edit ``scripts/initializedb.py``
@@ -76,25 +153,23 @@ 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`` and we'll change the very end
+of the script to create two ``User`` objects (``basic`` and ``editor``) and a
+``Page`` rather than a ``MyModel`` and add them to our ``dbsession``.
-Open ``tutorial/tutorial/scripts/initializedb.py`` and edit it to look like
+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: 16,31,41
+ :emphasize-lines: 18,44-57
-Only the highlighted lines need to be changed, as well as removing the lines
-referencing ``pyramid.scripts.common`` and ``options`` under the ``main``
-function.
+Only the highlighted lines need to be changed.
Installing the project and re-initializing the database
@@ -107,28 +182,49 @@ made to both the models.py file and to the initializedb.py file. See
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
---------------------------------
@@ -144,3 +240,14 @@ up with a Python traceback on your console that ends with this exception:
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 08fa8f16b..184f9e1fa 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -14,13 +14,12 @@ 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
-===============================================
+Adding the ``docutils`` dependency
+==================================
-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.
+Remember in the previous chapter we added a new dependency on 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,44 +29,91 @@ 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.
-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.
+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.
-Make sure your current working directory is the root of the project (the
-directory in which ``setup.py`` lives) and execute the following command.
+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.
-On UNIX:
+Adding routes to ``routes.py``
+==============================
-.. code-block:: bash
+This is the URL Dispatch tutorial and so let's start by adding some
+URL patterns to our app. Later we'll attach views to handle the URLs.
- $ cd tutorial
- $ $VENV/bin/python setup.py develop
+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 Windows:
+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``. 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'``.
+
+#. 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'``.
+
+#. 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'``.
+
+As a result of our edits, the ``routes.py`` file should look something like:
+
+.. literalinclude:: src/views/tutorial/routes.py
+ :linenos:
+ :emphasize-lines: 3-6
+ :language: python
- c:\pyramidtut> cd tutorial
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+The highlighted lines are the ones that need to be added or edited.
-Success executing this command will end with a line to the console something
-like this::
+.. warning::
- Finished processing dependencies for tutorial==0.0
+ 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 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.
Adding view functions in ``views/default.py``
=============================================
-It's time for a major change. Open ``tutorial/tutorial/views/default.py`` and
+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
@@ -81,7 +127,7 @@ 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. We also delated the ``db_err_msg``
+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/default.py``
@@ -96,11 +142,12 @@ We'll describe each one briefly in the following sections.
.. note::
- There is nothing special about the filename ``default.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
+ 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.
+ as in our case), but this is only by convention, not a requirement.
The ``view_wiki`` view function
-------------------------------
@@ -109,20 +156,20 @@ Following is the code for the ``view_wiki`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views/default.py
:lines: 17-20
- :lineno-start: 17
+ :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.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
+: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.
@@ -133,7 +180,7 @@ Here is the code for the ``view_page`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views/default.py
:lines: 22-42
- :lineno-start: 22
+ :lineno-match:
:linenos:
:language: python
@@ -142,12 +189,12 @@ 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
@@ -159,22 +206,76 @@ 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.jinja2`` template, as indicated in
+renderer used will be the ``view.jinja2`` template, as indicated in
the ``@view_config`` decorator that is applied to ``view_page()``.
+If the page does not exist then we need to handle that by raising
+:class:`pyramid.httpexceptions.HTTPNotFound`` to trigger our 404 handling
+defined in ``tutorial/views/notfound.py``.
+
+.. note::
+
+ 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 probably the problem! See
+ :ref:`special_exceptions_in_callables` for more information about
+ exception views.
+
+The ``edit_page`` view function
+-------------------------------
+
+Here is the code for the ``edit_page`` view function and its decorator:
+
+.. 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.
+
+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
+``body`` element of the request parameters and sets it as the ``data``
+attribute of the page object. It then redirects to the ``view_page`` view
+of the wiki 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
+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: 44-55
- :lineno-start: 44
+ :lines: 58-70
+ :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 ``check`` function
+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
@@ -190,8 +291,12 @@ 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``. We then redirect back to the ``view_page`` view for
-the newly created page.
+``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 so we query 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
@@ -203,34 +308,6 @@ 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/default.py
- :lines: 57-69
- :lineno-start: 57
- :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.
-
-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
-``body`` element of the request parameters and sets it as the ``data``
-attribute of the page object. It then redirects to the ``view_page`` view
-of the wiki 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
-simply renders the edit form, passing the page object and a ``save_url``
-which will be used as the action of the generated form.
-
Adding templates
================
@@ -240,113 +317,119 @@ 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 ``layout.jinja2`` template
+------------------------------
+
+Replace ``tutorial/templates/layout.jinja2`` with the following content:
+
+.. literalinclude:: src/views/tutorial/templates/layout.jinja2
+ :linenos:
+ :emphasize-lines: 11,36
+ :language: html
+
+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.
+
+- We have defined 2 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 ``view.jinja2`` template
----------------------------
-Create ``tutorial/tutorial/templates/view.jinja2`` and add the following
-content:
+Create ``tutorial/templates/view.jinja2`` and add the following content:
.. literalinclude:: src/views/tutorial/templates/view.jinja2
:linenos:
- :emphasize-lines: 36,38-40
+ :emphasize-lines: 1,3,6
:language: html
-This template is used by ``view_page()`` for displaying a single
-wiki page. It includes:
+This template is used by ``view_page()`` for displaying a single wiki page.
+It includes:
+- 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 to insert the
+ page name of the page into the page's title (line 3).
+- We override the ``content`` block from the base layout to insert our markup
+ into the body (line 5-18).
- A variable that is replaced with the ``content`` value provided by the view
- (line 36). ``content`` contains HTML, so the ``|safe`` filter is used to
+ (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to
prevent escaping it (e.g., changing ">" to "&gt;").
- A link that points at the "edit" URL which invokes the ``edit_page`` view for
- the page being viewed (lines 38-40).
+ the page being viewed (line 9).
The ``edit.jinja2`` template
----------------------------
-Create ``tutorial/tutorial/templates/edit.jinja2`` and add the following
-content:
+Create ``tutorial/templates/edit.jinja2`` and add the following content:
.. literalinclude:: src/views/tutorial/templates/edit.jinja2
:linenos:
- :emphasize-lines: 42,44,47
+ :emphasize-lines: 3,12,14,17
: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 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 that includes:
+- Again, we are extending 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 44).
-- A submit button that has the name ``form.submitted`` (line 47).
+ 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
-42). The view will use the ``body`` and ``form.submitted`` values.
+12). 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.
-
-Static assets
--------------
+The ``404.jinja2`` template
+---------------------------
-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 ``__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.
+Replace ``tutorial/templates/404.jinja2`` with the following content:
-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'``.
-
-#. 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'``.
-
-#. 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'``.
+.. 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: 11-14
+ :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, you 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
====================================
@@ -370,3 +453,5 @@ each of the following URLs, checking that the result is as expected:
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 8e3bb4c13..45e2fddd0 100644
--- a/docs/tutorials/wiki2/design.rst
+++ b/docs/tutorials/wiki2/design.rst
@@ -6,7 +6,7 @@ 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``
@@ -14,16 +14,24 @@ Python module. We will add this module in the dependency list on the project
``setup.py`` file.
Models
-------
+======
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`.
@@ -32,8 +40,8 @@ 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
@@ -45,38 +53,46 @@ 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/default.py`` subpackage and 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:
@@ -102,7 +118,7 @@ in the following table:
| | submitted, redirect | | | |
| | to /PageName | | | |
+----------------------+-----------------------+-------------+----------------+------------+
-| /add_page/PageName | Create the page | add_page | edit.jinja2 | edit |
+| /add_page/PageName | Create the page | add_page | edit.jinja2 | create |
| | *PageName* in | | | |
| | storage, display | | | |
| | the edit form | | | |
diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst
index fee50a1cf..ec90859a9 100644
--- a/docs/tutorials/wiki2/distributing.rst
+++ b/docs/tutorials/wiki2/distributing.rst
@@ -4,9 +4,8 @@ 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:
diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst
index 0a3873dcd..74fb5bfd5 100644
--- a/docs/tutorials/wiki2/index.rst
+++ b/docs/tutorials/wiki2/index.rst
@@ -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 047c66c06..5d6d8e56b 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -298,6 +298,13 @@ Initializing the database
We need to use the ``initialize_tutorial_db`` :term:`console
script` to initialize our database.
+.. note::
+
+ The ``initialize_tutorial_db`` command is not performing a migration but
+ rather simply creating missing tables and adding 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):
@@ -397,12 +404,17 @@ 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 ``zope.sqlalchemy`` 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 the tutorial. See :ref:`available_template_system_bindings` for some
+ options.
.. note::
@@ -411,3 +423,15 @@ assumptions:
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..6fb32a1b2
--- /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.encode('utf8')
+ 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/authorization/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py
index 5bb534f79..5bb534f79 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py
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/authentication/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..44d14304e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
@@ -0,0 +1,64 @@
+<!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>{% 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">
+
+ <!-- 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">
+ {% 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>
+ <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/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..ffe967f6e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
@@ -0,0 +1,76 @@
+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 '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 99c4ff0fe..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
diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini
index 97acfbd7d..686dba48a 100644
--- a/docs/tutorials/wiki2/src/authorization/production.ini
+++ b/docs/tutorials/wiki2/src/authorization/production.ini
@@ -11,11 +11,11 @@ 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
diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py
index d4e5a4072..57538f2d0 100644
--- a/docs/tutorials/wiki2/src/authorization/setup.py
+++ b/docs/tutorials/wiki2/src/authorization/setup.py
@@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
'pyramid_jinja2',
'pyramid_debugtoolbar',
@@ -17,9 +19,12 @@ requires = [
'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 084fee19f..f5c033b8b 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
@@ -1,27 +1,13 @@
from pyramid.config import Configurator
-from pyramid.authentication import AuthTktAuthenticationPolicy
-from pyramid.authorization import ACLAuthorizationPolicy
-from security.default import groupfinder
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- authn_policy = AuthTktAuthenticationPolicy(
- 'sosecret', callback=groupfinder, hashalg='sha512')
- authz_policy = ACLAuthorizationPolicy()
- config = Configurator(settings=settings,
- root_factory='tutorial.models.mymodel.RootFactory')
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(authz_policy)
+ config = Configurator(settings=settings)
config.include('pyramid_jinja2')
- config.include('.models.meta')
- 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.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/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
index 7b1c62867..a8871f6f5 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
@@ -1,7 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
-# import all models classes here for sqlalchemy mappers
-# to pick up
-from .mymodel import Page # flake8: noqa
+import zope.sqlalchemy
-# run configure mappers to ensure we avoid any race conditions
+# 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
index 80ececd8c..fc3e8f1dd 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
@@ -1,8 +1,5 @@
-from sqlalchemy import engine_from_config
from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker
from sqlalchemy.schema import MetaData
-import zope.sqlalchemy
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
@@ -17,33 +14,3 @@ NAMING_CONVENTION = {
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Base = declarative_base(metadata=metadata)
-
-
-def includeme(config):
- settings = config.get_settings()
- dbmaker = get_dbmaker(get_engine(settings))
-
- config.add_request_method(
- lambda r: get_session(r.tm, dbmaker),
- 'dbsession',
- reify=True
- )
-
- config.include('pyramid_tm')
-
-
-def get_session(transaction_manager, dbmaker):
- dbsession = dbmaker()
- zope.sqlalchemy.register(dbsession,
- transaction_manager=transaction_manager)
- return dbsession
-
-
-def get_engine(settings, prefix='sqlalchemy.'):
- return engine_from_config(settings, prefix)
-
-
-def get_dbmaker(engine):
- dbmaker = sessionmaker()
- dbmaker.configure(bind=engine)
- return dbmaker
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py
deleted file mode 100644
index 03e2f90ca..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from .meta import Base
-
-from pyramid.security import (
- Allow,
- Everyone,
- )
-
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-
-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(Integer)
-
-class RootFactory(object):
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
- def __init__(self, request):
- pass \ No newline at end of file
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..6fb32a1b2
--- /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.encode('utf8')
+ 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..c7c3a2120
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
@@ -0,0 +1,50 @@
+from pyramid.httpexceptions import HTTPNotFound
+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']
+ 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 4aac4a848..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
@@ -7,13 +7,15 @@ from pyramid.paster import (
setup_logging,
)
-from ..models.meta import (
- Base,
- get_session,
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
-from ..models.mymodel import Page
+from ..models import Page, User
def usage(argv):
@@ -27,16 +29,29 @@ 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)
+ settings = get_appsettings(config_uri, options=options)
engine = get_engine(settings)
- dbmaker = get_dbmaker(engine)
-
- dbsession = get_session(transaction.manager, dbmaker)
-
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
new file mode 100644
index 000000000..25cff7b05
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
@@ -0,0 +1,40 @@
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+)
+
+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/security/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py
deleted file mode 100644
index d88c9c71f..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py
+++ /dev/null
@@ -1,7 +0,0 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
-GROUPS = {'editor':['group:editors']}
-
-def groupfinder(userid, request):
- if userid in USERS:
- return GROUPS.get(userid, [])
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/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
index c4f3a2c93..7db25c674 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
@@ -1,73 +1,20 @@
-<!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>Edit{% if page.name %} {{page.name}}{% endif %} - 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">
- {% if logged_in %}
- <p class="pull-right">
- <a href="{{ request.application_url }}/logout">Logout</a>
- </p>
- {% endif %}
- <p>
- Editing <strong>{% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}</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" rows="10" cols="60">{{ page.data }}</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>
+{% 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/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
index ff624c65b..44d14304e 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
@@ -8,7 +8,7 @@
<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>
+ <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">
@@ -32,20 +32,18 @@
<img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- {% block content %}
- <p>No content</p>
- {% endblock content %}
- </div>
- </div>
- <div class="row">
- <div class="links">
- <ul>
- <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>
- </ul>
+ <div class="content">
+ {% 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>
<div class="row">
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
index a80a2a165..1806de0ff 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
@@ -1,74 +1,26 @@
-<!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>
- {{ message }}
- </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>
+{% 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/mytemplate.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2
deleted file mode 100644
index bb622bf5a..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2
+++ /dev/null
@@ -1,8 +0,0 @@
-{% 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/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2
index a7afc66fc..94419e228 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2
@@ -1,71 +1,18 @@
-<!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">
- {% if logged_in %}
- <p class="pull-right">
- <a href="{{ request.application_url }}/logout">Logout</a>
- </p>
- {% endif %}
- <p>{{ content|safe }}</p>
- <p>
- <a href="{{ edit_url }}">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong>{% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}</strong>
- </p>
- <p>You can return to the
- <a href="{{request.application_url}}">FrontPage</a>.
- </p>
- </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>
+{% 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 b947e3bb1..c54945c28 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
@@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase):
self.config = testing.setUp(settings={
'sqlalchemy.url': 'sqlite:///:memory:'
})
- self.config.include('.models.meta')
+ self.config.include('.models')
settings = self.config.get_settings()
- from .models.meta import (
- get_session,
+ from .models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
self.engine = get_engine(settings)
- dbmaker = get_dbmaker(self.engine)
+ session_factory = get_session_factory(self.engine)
- self.session = get_session(transaction.manager, dbmaker)
+ self.session = get_tm_session(session_factory, transaction.manager)
def init_database(self):
- from .models.meta import Base
+ from .models import Base
Base.metadata.create_all(self.engine)
def tearDown(self):
@@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase):
testing.tearDown()
transaction.abort()
- Base.metadata.create_all(self.engine)
+ Base.metadata.drop_all(self.engine)
class TestMyViewSuccessCondition(BaseTest):
@@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest):
super(TestMyViewSuccessCondition, self).setUp()
self.init_database()
- from .models.mymodel import MyModel
+ from .models import MyModel
model = MyModel(name='one', value=55)
self.session.add(model)
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
index f35f041a4..9358993ea 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
@@ -2,43 +2,25 @@ import cgi
import re
from docutils.core import publish_parts
-from pyramid.httpexceptions import (
- HTTPFound,
- HTTPNotFound,
- )
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
-from pyramid.view import (
- view_config,
- forbidden_view_config,
- )
-
-from pyramid.security import (
- remember,
- forget,
- )
-
-from ..security.default import USERS
-
-from ..models.mymodel import Page
+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',
- permission='view')
+@view_config(route_name='view_wiki')
def view_wiki(request):
- return HTTPFound(location=request.route_url('view_page',
- pagename='FrontPage'))
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
-@view_config(route_name='view_page', renderer='templates/view.jinja2',
+@view_config(route_name='view_page', renderer='../templates/view.jinja2',
permission='view')
def view_page(request):
- pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).first()
- if page is None:
- return HTTPNotFound('No such page')
+ page = request.context.page
- def check(match):
+ def add_link(match):
word = match.group(1)
exists = request.dbsession.query(Page).filter_by(name=word).all()
if exists:
@@ -49,72 +31,34 @@ def view_page(request):
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,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='add_page', renderer='templates/edit.jinja2',
- 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)
- request.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)
+ 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',
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2',
permission='edit')
def edit_page(request):
- pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ page = request.context.page
if 'form.submitted' in request.params:
page.data = request.params['body']
- request.dbsession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
return dict(
- page=page,
- save_url = request.route_url('edit_page', pagename=pagename),
- logged_in=request.authenticated_userid
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
)
-@view_config(route_name='login', renderer='templates/login.jinja2')
-@forbidden_view_config(renderer='templates/login.jinja2')
-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 = ''
+@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:
- 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)
+ 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/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini
index 97acfbd7d..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
diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py
index eb771010f..7bc697730 100644
--- a/docs/tutorials/wiki2/src/basiclayout/setup.py
+++ b/docs/tutorials/wiki2/src/basiclayout/setup.py
@@ -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 7994bbfa8..4dab44823 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
@@ -6,8 +6,7 @@ def main(global_config, **settings):
"""
config = Configurator(settings=settings)
config.include('pyramid_jinja2')
- config.include('.models.meta')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('home', '/')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
index 6ffc10a78..48a957ecb 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
@@ -1,7 +1,73 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
-# import all models classes here for sqlalchemy mappers
-# to pick up
+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 to ensure we avoid any race conditions
+# 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
index 80ececd8c..fc3e8f1dd 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
@@ -1,8 +1,5 @@
-from sqlalchemy import engine_from_config
from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker
from sqlalchemy.schema import MetaData
-import zope.sqlalchemy
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
@@ -17,33 +14,3 @@ NAMING_CONVENTION = {
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Base = declarative_base(metadata=metadata)
-
-
-def includeme(config):
- settings = config.get_settings()
- dbmaker = get_dbmaker(get_engine(settings))
-
- config.add_request_method(
- lambda r: get_session(r.tm, dbmaker),
- 'dbsession',
- reify=True
- )
-
- config.include('pyramid_tm')
-
-
-def get_session(transaction_manager, dbmaker):
- dbsession = dbmaker()
- zope.sqlalchemy.register(dbsession,
- transaction_manager=transaction_manager)
- return dbsession
-
-
-def get_engine(settings, prefix='sqlalchemy.'):
- return engine_from_config(settings, prefix)
-
-
-def get_dbmaker(engine):
- dbmaker = sessionmaker()
- dbmaker.configure(bind=engine)
- return dbmaker
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py
index 5a2b5890c..d65a01a42 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py
@@ -1,4 +1,3 @@
-from .meta import Base
from sqlalchemy import (
Column,
Index,
@@ -6,6 +5,8 @@ from sqlalchemy import (
Text,
)
+from .meta import Base
+
class MyModel(Base):
__tablename__ = 'models'
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 f0d09729e..7307ecc5c 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
@@ -9,13 +9,13 @@ from pyramid.paster import (
from pyramid.scripts.common import parse_vars
-from ..models.meta import (
- Base,
- get_session,
+from ..models.meta import Base
+from ..models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
-from ..models.mymodel import MyModel
+from ..models import MyModel
def usage(argv):
@@ -34,12 +34,12 @@ def main(argv=sys.argv):
settings = get_appsettings(config_uri, options=options)
engine = get_engine(settings)
- dbmaker = get_dbmaker(engine)
-
- dbsession = get_session(transaction.manager, dbmaker)
-
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)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2
index bb622bf5a..1917f83c7 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2
@@ -3,6 +3,6 @@
{% 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>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
</div>
{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
index b947e3bb1..c54945c28 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
@@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase):
self.config = testing.setUp(settings={
'sqlalchemy.url': 'sqlite:///:memory:'
})
- self.config.include('.models.meta')
+ self.config.include('.models')
settings = self.config.get_settings()
- from .models.meta import (
- get_session,
+ from .models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
self.engine = get_engine(settings)
- dbmaker = get_dbmaker(self.engine)
+ session_factory = get_session_factory(self.engine)
- self.session = get_session(transaction.manager, dbmaker)
+ self.session = get_tm_session(session_factory, transaction.manager)
def init_database(self):
- from .models.meta import Base
+ from .models import Base
Base.metadata.create_all(self.engine)
def tearDown(self):
@@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase):
testing.tearDown()
transaction.abort()
- Base.metadata.create_all(self.engine)
+ Base.metadata.drop_all(self.engine)
class TestMyViewSuccessCondition(BaseTest):
@@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest):
super(TestMyViewSuccessCondition, self).setUp()
self.init_database()
- from .models.mymodel import MyModel
+ from .models import MyModel
model = MyModel(name='one', value=55)
self.session.add(model)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
index 13ad8793c..ad0c728d7 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
@@ -3,7 +3,7 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from ..models.mymodel import MyModel
+from ..models import MyModel
@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
@@ -12,7 +12,7 @@ def my_view(request):
query = request.dbsession.query(MyModel)
one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
- return Response(db_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'}
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/production.ini b/docs/tutorials/wiki2/src/models/production.ini
index 97acfbd7d..cb1db3211 100644
--- a/docs/tutorials/wiki2/src/models/production.ini
+++ b/docs/tutorials/wiki2/src/models/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
diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py
index eb771010f..bdc9ceed7 100644
--- a/docs/tutorials/wiki2/src/models/setup.py
+++ b/docs/tutorials/wiki2/src/models/setup.py
@@ -9,6 +9,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
'pyramid',
'pyramid_jinja2',
'pyramid_debugtoolbar',
@@ -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 7994bbfa8..4dab44823 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
@@ -6,8 +6,7 @@ def main(global_config, **settings):
"""
config = Configurator(settings=settings)
config.include('pyramid_jinja2')
- config.include('.models.meta')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('home', '/')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
index 7b1c62867..a8871f6f5 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
@@ -1,7 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
-# import all models classes here for sqlalchemy mappers
-# to pick up
-from .mymodel import Page # flake8: noqa
+import zope.sqlalchemy
-# run configure mappers to ensure we avoid any race conditions
+# 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
index 80ececd8c..fc3e8f1dd 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
@@ -1,8 +1,5 @@
-from sqlalchemy import engine_from_config
from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker
from sqlalchemy.schema import MetaData
-import zope.sqlalchemy
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
@@ -17,33 +14,3 @@ NAMING_CONVENTION = {
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Base = declarative_base(metadata=metadata)
-
-
-def includeme(config):
- settings = config.get_settings()
- dbmaker = get_dbmaker(get_engine(settings))
-
- config.add_request_method(
- lambda r: get_session(r.tm, dbmaker),
- 'dbsession',
- reify=True
- )
-
- config.include('pyramid_tm')
-
-
-def get_session(transaction_manager, dbmaker):
- dbsession = dbmaker()
- zope.sqlalchemy.register(dbsession,
- transaction_manager=transaction_manager)
- return dbsession
-
-
-def get_engine(settings, prefix='sqlalchemy.'):
- return engine_from_config(settings, prefix)
-
-
-def get_dbmaker(engine):
- dbmaker = sessionmaker()
- dbmaker.configure(bind=engine)
- return dbmaker
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py
deleted file mode 100644
index 45571d78e..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from .meta import Base
-from sqlalchemy import (
- Column,
- Integer,
- Text,
-)
-
-
-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(Integer)
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..6fb32a1b2
--- /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.encode('utf8')
+ 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 4aac4a848..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
@@ -7,13 +7,15 @@ from pyramid.paster import (
setup_logging,
)
-from ..models.meta import (
- Base,
- get_session,
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
-from ..models.mymodel import Page
+from ..models import Page, User
def usage(argv):
@@ -27,16 +29,29 @@ 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)
+ settings = get_appsettings(config_uri, options=options)
engine = get_engine(settings)
- dbmaker = get_dbmaker(engine)
-
- dbsession = get_session(transaction.manager, dbmaker)
-
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/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2
index bb622bf5a..1917f83c7 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2
@@ -3,6 +3,6 @@
{% 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>
+ <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/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py
index b947e3bb1..c54945c28 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py
@@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase):
self.config = testing.setUp(settings={
'sqlalchemy.url': 'sqlite:///:memory:'
})
- self.config.include('.models.meta')
+ self.config.include('.models')
settings = self.config.get_settings()
- from .models.meta import (
- get_session,
+ from .models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
self.engine = get_engine(settings)
- dbmaker = get_dbmaker(self.engine)
+ session_factory = get_session_factory(self.engine)
- self.session = get_session(transaction.manager, dbmaker)
+ self.session = get_tm_session(session_factory, transaction.manager)
def init_database(self):
- from .models.meta import Base
+ from .models import Base
Base.metadata.create_all(self.engine)
def tearDown(self):
@@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase):
testing.tearDown()
transaction.abort()
- Base.metadata.create_all(self.engine)
+ Base.metadata.drop_all(self.engine)
class TestMyViewSuccessCondition(BaseTest):
@@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest):
super(TestMyViewSuccessCondition, self).setUp()
self.init_database()
- from .models.mymodel import MyModel
+ from .models import MyModel
model = MyModel(name='one', value=55)
self.session.add(model)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
index 13ad8793c..ad0c728d7 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
@@ -3,7 +3,7 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from ..models.mymodel import MyModel
+from ..models import MyModel
@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
@@ -12,7 +12,7 @@ def my_view(request):
query = request.dbsession.query(MyModel)
one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
- return Response(db_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'}
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 99c4ff0fe..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
diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini
index 97acfbd7d..686dba48a 100644
--- a/docs/tutorials/wiki2/src/tests/production.ini
+++ b/docs/tutorials/wiki2/src/tests/production.ini
@@ -11,11 +11,11 @@ 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
diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py
index f640b4399..57538f2d0 100644
--- a/docs/tutorials/wiki2/src/tests/setup.py
+++ b/docs/tutorials/wiki2/src/tests/setup.py
@@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
'pyramid_jinja2',
'pyramid_debugtoolbar',
@@ -17,10 +19,12 @@ requires = [
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
- 'WebTest',
]
+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 084fee19f..f5c033b8b 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
@@ -1,27 +1,13 @@
from pyramid.config import Configurator
-from pyramid.authentication import AuthTktAuthenticationPolicy
-from pyramid.authorization import ACLAuthorizationPolicy
-from security.default import groupfinder
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- authn_policy = AuthTktAuthenticationPolicy(
- 'sosecret', callback=groupfinder, hashalg='sha512')
- authz_policy = ACLAuthorizationPolicy()
- config = Configurator(settings=settings,
- root_factory='tutorial.models.mymodel.RootFactory')
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(authz_policy)
+ config = Configurator(settings=settings)
config.include('pyramid_jinja2')
- config.include('.models.meta')
- 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.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/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
index 7b1c62867..a8871f6f5 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
@@ -1,7 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
-# import all models classes here for sqlalchemy mappers
-# to pick up
-from .mymodel import Page # flake8: noqa
+import zope.sqlalchemy
-# run configure mappers to ensure we avoid any race conditions
+# 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
index 80ececd8c..fc3e8f1dd 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
@@ -1,8 +1,5 @@
-from sqlalchemy import engine_from_config
from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker
from sqlalchemy.schema import MetaData
-import zope.sqlalchemy
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
@@ -17,33 +14,3 @@ NAMING_CONVENTION = {
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Base = declarative_base(metadata=metadata)
-
-
-def includeme(config):
- settings = config.get_settings()
- dbmaker = get_dbmaker(get_engine(settings))
-
- config.add_request_method(
- lambda r: get_session(r.tm, dbmaker),
- 'dbsession',
- reify=True
- )
-
- config.include('pyramid_tm')
-
-
-def get_session(transaction_manager, dbmaker):
- dbsession = dbmaker()
- zope.sqlalchemy.register(dbsession,
- transaction_manager=transaction_manager)
- return dbsession
-
-
-def get_engine(settings, prefix='sqlalchemy.'):
- return engine_from_config(settings, prefix)
-
-
-def get_dbmaker(engine):
- dbmaker = sessionmaker()
- dbmaker.configure(bind=engine)
- return dbmaker
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py
deleted file mode 100644
index 03e2f90ca..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from .meta import Base
-
-from pyramid.security import (
- Allow,
- Everyone,
- )
-
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-
-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(Integer)
-
-class RootFactory(object):
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
- def __init__(self, request):
- pass \ No newline at end of file
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..6fb32a1b2
--- /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.encode('utf8')
+ 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..c7c3a2120
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py
@@ -0,0 +1,50 @@
+from pyramid.httpexceptions import HTTPNotFound
+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']
+ 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 4aac4a848..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
@@ -7,13 +7,15 @@ from pyramid.paster import (
setup_logging,
)
-from ..models.meta import (
- Base,
- get_session,
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
-from ..models.mymodel import Page
+from ..models import Page, User
def usage(argv):
@@ -27,16 +29,29 @@ 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)
+ settings = get_appsettings(config_uri, options=options)
engine = get_engine(settings)
- dbmaker = get_dbmaker(engine)
-
- dbsession = get_session(transaction.manager, dbmaker)
-
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
new file mode 100644
index 000000000..25cff7b05
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py
@@ -0,0 +1,40 @@
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+)
+
+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/security/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py
deleted file mode 100644
index 5bb534f79..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# package
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py
deleted file mode 100644
index d88c9c71f..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py
+++ /dev/null
@@ -1,7 +0,0 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
-GROUPS = {'editor':['group:editors']}
-
-def groupfinder(userid, request):
- if userid in USERS:
- return GROUPS.get(userid, [])
diff --git a/docs/tutorials/wiki2/src/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
index c4f3a2c93..7db25c674 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2
@@ -1,73 +1,20 @@
-<!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>Edit{% if page.name %} {{page.name}}{% endif %} - 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">
- {% if logged_in %}
- <p class="pull-right">
- <a href="{{ request.application_url }}/logout">Logout</a>
- </p>
- {% endif %}
- <p>
- Editing <strong>{% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}</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" rows="10" cols="60">{{ page.data }}</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>
+{% 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/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
index ff624c65b..44d14304e 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
@@ -8,7 +8,7 @@
<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>
+ <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">
@@ -32,20 +32,18 @@
<img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- {% block content %}
- <p>No content</p>
- {% endblock content %}
- </div>
- </div>
- <div class="row">
- <div class="links">
- <ul>
- <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>
- </ul>
+ <div class="content">
+ {% 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>
<div class="row">
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2
index a80a2a165..1806de0ff 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2
@@ -1,74 +1,26 @@
-<!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>
- {{ message }}
- </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>
+{% 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/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2
index a7afc66fc..94419e228 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2
@@ -1,71 +1,18 @@
-<!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">
- {% if logged_in %}
- <p class="pull-right">
- <a href="{{ request.application_url }}/logout">Logout</a>
- </p>
- {% endif %}
- <p>{{ content|safe }}</p>
- <p>
- <a href="{{ edit_url }}">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong>{% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}</strong>
- </p>
- <p>You can return to the
- <a href="{{request.application_url}}">FrontPage</a>.
- </p>
- </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>
+{% 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/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py
index 8b1378917..e69de29bb 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py
@@ -1 +0,0 @@
-
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
index 339c60bc2..b2c6e0975 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
@@ -1,81 +1,57 @@
+import transaction
import unittest
-
-from pyramid import testing
-
-
-def dummy_request(dbsession):
- return testing.DummyRequest(dbsession=dbsession)
-
-
-def _register_routes(config):
- config.add_route('view_page', '{pagename}')
- config.add_route('edit_page', '{pagename}/edit_page')
- config.add_route('add_page', 'add_page/{pagename}')
+from webtest import TestApp
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'
-
- engine = None
-
- @staticmethod
- def setup_database():
- import transaction
- from tutorial.models.mymodel import Page
- from tutorial.models.meta import (
- Base,
- )
- import tutorial.models.meta
-
-
- def initialize_db(dbsession, engine):
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- dbsession.add(model)
-
- def wrap_get_session(transaction_manager, dbmaker):
- dbsession = get_session(transaction_manager, dbmaker)
- initialize_db(dbsession, engine)
- tutorial.models.meta.get_session = get_session
- tutorial.models.meta.get_engine = get_engine
- return dbsession
-
- def wrap_get_engine(settings):
- global engine
- engine = get_engine(settings)
- return engine
-
- get_session = tutorial.models.meta.get_session
- tutorial.models.meta.get_session = wrap_get_session
-
- get_engine = tutorial.models.meta.get_engine
- tutorial.models.meta.get_engine = wrap_get_engine
+ 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):
- cls.setup_database()
-
- from webtest import TestApp
+ from tutorial.models.meta import Base
+ from tutorial.models import (
+ User,
+ Page,
+ get_tm_session,
+ )
from tutorial import main
- settings = {'sqlalchemy.url': 'sqlite://'}
+
+ 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(engine)
-
- def tearDown(self):
- import transaction
- transaction.abort()
+ Base.metadata.drop_all(bind=cls.engine)
def test_root(self):
res = self.testapp.get('/', status=302)
@@ -89,41 +65,46 @@ class FunctionalTests(unittest.TestCase):
self.testapp.get('/SomePage', status=404)
def test_successful_log_in(self):
- res = self.testapp.get(self.viewer_login, status=302)
+ 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.viewer_wrong_login, status=200)
+ 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.viewer_login, status=302)
+ 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.viewer_login, status=302)
+ 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=200)
+ 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=200)
+ res = self.testapp.get('/add_page/NewPage', status=302).follow()
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)
+ 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_viewer_user_cannot_add(self):
- self.testapp.get(self.viewer_login, status=302)
+ 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'Login' in res.body)
+ self.assertTrue(b'Editing' in res.body)
def test_editors_member_user_can_edit(self):
self.testapp.get(self.editor_login, status=302)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py
index d70311e38..5253183df 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py
@@ -8,48 +8,45 @@ def dummy_request(dbsession):
return testing.DummyRequest(dbsession=dbsession)
-def _register_routes(config):
- config.add_route('view_page', '{pagename}')
- config.add_route('edit_page', '{pagename}/edit_page')
- config.add_route('add_page', 'add_page/{pagename}')
-
-
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.meta')
- _register_routes(self.config)
- settings = self.config.get_settings()
-
- from ..models.meta import (
- get_session,
- get_engine,
- get_dbmaker,
- )
-
- self.engine = get_engine(settings)
- dbmaker = get_dbmaker(self.engine)
+ self.config.include('..models')
+ self.config.include('..routes')
- self.session = get_session(transaction.manager, dbmaker)
+ 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
- Base.metadata.create_all(self.engine)
+ 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):
+class ViewWikiTests(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
- _register_routes(self.config)
+ self.config.include('..routes')
def tearDown(self):
testing.tearDown()
@@ -65,26 +62,21 @@ class ViewWikiTests(unittest.TestCase):
class ViewPageTests(BaseTest):
- def setUp(self):
- super(ViewPageTests, self).setUp()
-
- def tearDown(self):
- transaction.abort()
- testing.tearDown()
-
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
- from ..models.mymodel import Page
- page = Page(name='IDoExist', data='Hello CruelWorld IDoExist')
- self.session.add(page)
+ 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.matchdict['pagename'] = 'IDoExist'
+ request.context = PageResource(page)
# call the view we're testing and check its behavior
info = self._callFUT(request)
@@ -102,67 +94,63 @@ class ViewPageTests(BaseTest):
class AddPageTests(BaseTest):
- def setUp(self):
- super(AddPageTests, self).setUp()
-
- def tearDown(self):
- transaction.abort()
- testing.tearDown()
-
def _callFUT(self, request):
from tutorial.views.default import add_page
return add_page(request)
def test_it_notsubmitted(self):
+ from ..routes import NewPage
request = dummy_request(self.session)
- request.matchdict = {'pagename': 'AnotherPage'}
+ request.user = self.makeUser('foo', 'editor')
+ request.context = NewPage('AnotherPage')
info = self._callFUT(request)
- self.assertEqual(info['page'].data, '')
+ self.assertEqual(info['pagedata'], '')
self.assertEqual(info['save_url'],
'http://example.com/add_page/AnotherPage')
def test_it_submitted(self):
- from ..models.mymodel import Page
+ from ..models import Page
+ from ..routes import NewPage
request = testing.DummyRequest({'form.submitted': True,
'body': 'Hello yo!'},
dbsession=self.session)
- request.matchdict = {'pagename': 'AnotherPage'}
+ 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 setUp(self):
- super(EditPageTests, self).setUp()
-
- def tearDown(self):
- transaction.abort()
- testing.tearDown()
-
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):
- from ..models.mymodel import Page
+ user = self.makeUser('foo', 'editor')
+ page = self.makePage('abc', 'hello', user)
+ self.session.add_all([page, user])
+
request = dummy_request(self.session)
- request.matchdict = {'pagename': 'abc'}
- page = Page(name='abc', data='hello')
- self.session.add(page)
+ request.context = self.makeContext(page)
info = self._callFUT(request)
- self.assertEqual(info['page'], page)
+ self.assertEqual(info['pagename'], 'abc')
self.assertEqual(info['save_url'],
'http://example.com/abc/edit_page')
def test_it_submitted(self):
- from ..models.mymodel import Page
+ 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.matchdict = {'pagename': 'abc'}
- page = Page(name='abc', data='hello')
- self.session.add(page)
+ 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/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
index f35f041a4..9358993ea 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
@@ -2,43 +2,25 @@ import cgi
import re
from docutils.core import publish_parts
-from pyramid.httpexceptions import (
- HTTPFound,
- HTTPNotFound,
- )
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
-from pyramid.view import (
- view_config,
- forbidden_view_config,
- )
-
-from pyramid.security import (
- remember,
- forget,
- )
-
-from ..security.default import USERS
-
-from ..models.mymodel import Page
+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',
- permission='view')
+@view_config(route_name='view_wiki')
def view_wiki(request):
- return HTTPFound(location=request.route_url('view_page',
- pagename='FrontPage'))
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
-@view_config(route_name='view_page', renderer='templates/view.jinja2',
+@view_config(route_name='view_page', renderer='../templates/view.jinja2',
permission='view')
def view_page(request):
- pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).first()
- if page is None:
- return HTTPNotFound('No such page')
+ page = request.context.page
- def check(match):
+ def add_link(match):
word = match.group(1)
exists = request.dbsession.query(Page).filter_by(name=word).all()
if exists:
@@ -49,72 +31,34 @@ def view_page(request):
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,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='add_page', renderer='templates/edit.jinja2',
- 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)
- request.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)
+ 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',
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2',
permission='edit')
def edit_page(request):
- pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ page = request.context.page
if 'form.submitted' in request.params:
page.data = request.params['body']
- request.dbsession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
return dict(
- page=page,
- save_url = request.route_url('edit_page', pagename=pagename),
- logged_in=request.authenticated_userid
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
)
-@view_config(route_name='login', renderer='templates/login.jinja2')
-@forbidden_view_config(renderer='templates/login.jinja2')
-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 = ''
+@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:
- 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)
+ 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/production.ini b/docs/tutorials/wiki2/src/views/production.ini
index 97acfbd7d..cb1db3211 100644
--- a/docs/tutorials/wiki2/src/views/production.ini
+++ b/docs/tutorials/wiki2/src/views/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
diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py
index d4e5a4072..57538f2d0 100644
--- a/docs/tutorials/wiki2/src/views/setup.py
+++ b/docs/tutorials/wiki2/src/views/setup.py
@@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
'pyramid_jinja2',
'pyramid_debugtoolbar',
@@ -17,9 +19,12 @@ requires = [
'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 d28f09ca4..4dab44823 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
@@ -6,11 +6,7 @@ def main(global_config, **settings):
"""
config = Configurator(settings=settings)
config.include('pyramid_jinja2')
- config.include('.models.meta')
- 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('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
index 7b1c62867..a8871f6f5 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
@@ -1,7 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
-# import all models classes here for sqlalchemy mappers
-# to pick up
-from .mymodel import Page # flake8: noqa
+import zope.sqlalchemy
-# run configure mappers to ensure we avoid any race conditions
+# 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
index 80ececd8c..fc3e8f1dd 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
@@ -1,8 +1,5 @@
-from sqlalchemy import engine_from_config
from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker
from sqlalchemy.schema import MetaData
-import zope.sqlalchemy
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
@@ -17,33 +14,3 @@ NAMING_CONVENTION = {
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Base = declarative_base(metadata=metadata)
-
-
-def includeme(config):
- settings = config.get_settings()
- dbmaker = get_dbmaker(get_engine(settings))
-
- config.add_request_method(
- lambda r: get_session(r.tm, dbmaker),
- 'dbsession',
- reify=True
- )
-
- config.include('pyramid_tm')
-
-
-def get_session(transaction_manager, dbmaker):
- dbsession = dbmaker()
- zope.sqlalchemy.register(dbsession,
- transaction_manager=transaction_manager)
- return dbsession
-
-
-def get_engine(settings, prefix='sqlalchemy.'):
- return engine_from_config(settings, prefix)
-
-
-def get_dbmaker(engine):
- dbmaker = sessionmaker()
- dbmaker.configure(bind=engine)
- return dbmaker
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py
deleted file mode 100644
index 45571d78e..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from .meta import Base
-from sqlalchemy import (
- Column,
- Integer,
- Text,
-)
-
-
-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(Integer)
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..6fb32a1b2
--- /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.encode('utf8')
+ 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 4aac4a848..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
@@ -7,13 +7,15 @@ from pyramid.paster import (
setup_logging,
)
-from ..models.meta import (
- Base,
- get_session,
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
-from ..models.mymodel import Page
+from ..models import Page, User
def usage(argv):
@@ -27,16 +29,29 @@ 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)
+ settings = get_appsettings(config_uri, options=options)
engine = get_engine(settings)
- dbmaker = get_dbmaker(engine)
-
- dbsession = get_session(transaction.manager, dbmaker)
-
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
index b3aadfc2e..7db25c674 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2
@@ -1,68 +1,20 @@
-<!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>Edit{% if page.name %} {{page.name}}{% endif %} - 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>{% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}</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" rows="10" cols="60">{{ page.data }}</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>
+{% 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/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
index ff624c65b..71785157f 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
@@ -8,7 +8,7 @@
<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>
+ <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">
@@ -32,20 +32,9 @@
<img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- {% block content %}
- <p>No content</p>
- {% endblock content %}
- </div>
- </div>
- <div class="row">
- <div class="links">
- <ul>
- <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>
- </ul>
+ <div class="content">
+ {% block content %}{% endblock %}
+ </div>
</div>
</div>
<div class="row">
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2
index 36bb96870..94419e228 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2
@@ -1,66 +1,18 @@
-<!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>{{ content|safe }}</p>
- <p>
- <a href="{{ edit_url }}">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong>{% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}</strong>
- </p>
- <p>You can return to the
- <a href="{{request.application_url}}">FrontPage</a>.
- </p>
- </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>
+{% 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 b947e3bb1..c54945c28 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py
@@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase):
self.config = testing.setUp(settings={
'sqlalchemy.url': 'sqlite:///:memory:'
})
- self.config.include('.models.meta')
+ self.config.include('.models')
settings = self.config.get_settings()
- from .models.meta import (
- get_session,
+ from .models import (
get_engine,
- get_dbmaker,
+ get_session_factory,
+ get_tm_session,
)
self.engine = get_engine(settings)
- dbmaker = get_dbmaker(self.engine)
+ session_factory = get_session_factory(self.engine)
- self.session = get_session(transaction.manager, dbmaker)
+ self.session = get_tm_session(session_factory, transaction.manager)
def init_database(self):
- from .models.meta import Base
+ from .models import Base
Base.metadata.create_all(self.engine)
def tearDown(self):
@@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase):
testing.tearDown()
transaction.abort()
- Base.metadata.create_all(self.engine)
+ Base.metadata.drop_all(self.engine)
class TestMyViewSuccessCondition(BaseTest):
@@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest):
super(TestMyViewSuccessCondition, self).setUp()
self.init_database()
- from .models.mymodel import MyModel
+ from .models import MyModel
model = MyModel(name='one', value=55)
self.session.add(model)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
index 3e5c61a72..c1d402f6a 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
@@ -9,24 +9,24 @@ from pyramid.httpexceptions import (
from pyramid.view import view_config
-from ..models.mymodel import Page
+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):
- return HTTPFound(location=request.route_url('view_page',
- pagename='FrontPage'))
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
-@view_config(route_name='view_page', renderer='templates/view.jinja2')
+@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:
- return HTTPNotFound('No such page')
+ raise HTTPNotFound('No such page')
- def check(match):
+ def add_link(match):
word = match.group(1)
exists = request.dbsession.query(Page).filter_by(name=word).all()
if exists:
@@ -37,33 +37,34 @@ def view_page(request):
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)
+ 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='add_page', renderer='templates/edit.jinja2')
-def add_page(request):
- pagename = request.matchdict['pagename']
- if 'form.submitted' in request.params:
- body = request.params['body']
- page = Page(name=pagename, data=body)
- request.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.jinja2')
+@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']
- request.dbsession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
return dict(
- page=page,
- save_url = request.route_url('edit_page', pagename=pagename),
+ 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 '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 fe3fdaf2c..667550467 100644
--- a/docs/tutorials/wiki2/tests.rst
+++ b/docs/tutorials/wiki2/tests.rst
@@ -18,6 +18,14 @@ subpackage, and add several new tests.
Start by creating a new directory and 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 and so it will use the old code and you will wonder
+ why none of your changes are working!
+
Test the views
==============
@@ -35,7 +43,7 @@ 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
+the ``basic`` user cannot edit pages it didn't create, but the ``editor`` user
can, and so on.
@@ -57,39 +65,20 @@ follows:
:language: python
-Running the tests
-=================
+.. note::
-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
-<http://docs.pylonsproject.org/projects/webtest/en/latest/>`_, which we've used
-in our ``tests.py``. Change the ``requires`` list in ``setup.py`` to include
-``WebTest``.
+ 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``.
-.. literalinclude:: src/tests/setup.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:
-
-.. code-block:: bash
-
- $ $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:
@@ -114,3 +103,5 @@ The expected result should look like the following:
OK
Process finished with exit code 0
+
+.. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/