diff options
| author | Chris McDonough <chrism@plope.com> | 2011-06-11 05:43:16 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-06-11 05:43:16 -0400 |
| commit | a4d5525cdbb6b7e614939b20a340b989258779ca (patch) | |
| tree | 666586b1a0293a04fe3ed4bc27eeddbd680e4311 /docs | |
| parent | aee35e30083acd3d3c84e7f50db1f17bf6dc2d12 (diff) | |
| parent | b1b9f99e9a2e249cff61f4ccc0ecf10ac734fa08 (diff) | |
| download | pyramid-a4d5525cdbb6b7e614939b20a340b989258779ca.tar.gz pyramid-a4d5525cdbb6b7e614939b20a340b989258779ca.tar.bz2 pyramid-a4d5525cdbb6b7e614939b20a340b989258779ca.zip | |
Merge branch 'master' of github.com:Pylons/pyramid
Diffstat (limited to 'docs')
30 files changed, 474 insertions, 168 deletions
diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 5ee554284..3bd9c2a4e 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -86,9 +86,9 @@ that ends something like this: for action in resolveConflicts(self.actions): File "zope/configuration/config.py", line 1507, in resolveConflicts raise ConfigurationConflictError(conflicts) - zope.configuration.config.ConfigurationConflictError: + zope.configuration.config.ConfigurationConflictError: Conflicting configuration actions - For: ('view', None, '', None, <InterfaceClass pyramid.interfaces.IView>, + For: ('view', None, '', None, <InterfaceClass pyramid.interfaces.IView>, None, None, None, None, None, False, None, None, None) ('app.py', 14, '<module>', 'config.add_view(hello_world)') ('app.py', 17, '<module>', 'config.add_view(hello_world)') @@ -291,7 +291,7 @@ These are the methods of the configurator which provide conflict detection: :meth:`~pyramid.config.Configurator.add_route`, :meth:`~pyramid.config.Configurator.add_renderer`, :meth:`~pyramid.config.Configurator.set_request_factory`, -:meth:`~pyramid.config.Configurator.set_renderer_globals_factory` +:meth:`~pyramid.config.Configurator.set_renderer_globals_factory`, :meth:`~pyramid.config.Configurator.set_locale_negotiator` and :meth:`~pyramid.config.Configurator.set_default_permission`. @@ -425,7 +425,7 @@ For example: if __name__ == '__main__': config = Configurator() - config.add_directive('add_newrequest_subscriber', + config.add_directive('add_newrequest_subscriber', add_newrequest_subscriber) Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can @@ -450,7 +450,7 @@ code in a package named ``pyramid_subscriberhelpers``: :linenos: def includeme(config) - config.add_directive('add_newrequest_subscriber', + config.add_directive('add_newrequest_subscriber', add_newrequest_subscriber) The user of the add-on package ``pyramid_subscriberhelpers`` would then be diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index e15f7810c..3b938c09c 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -13,7 +13,7 @@ single: environment variables single: ini file settings single: PasteDeploy settings - + .. _environment_chapter: Environment Variables and ``.ini`` File Settings @@ -84,7 +84,7 @@ when this value is true. See also :ref:`debug_authorization_section`. | ``PYRAMID_DEBUG_AUTHORIZATION`` | ``debug_authorization`` | | | | | | | -| | | +| | | +---------------------------------+-----------------------------+ Debugging Not Found Errors @@ -259,7 +259,7 @@ List of string filter names that will be applied to all Mako expressions. Mako Import +++++++++++ -String list of Python statements, typically individual “import” lines, which +String list of Python statements, typically individual "import" lines, which will be placed into the module level preamble of all generated Python modules. @@ -330,7 +330,7 @@ settings that do not start with ``debug_*`` such as ``reload_templates``. If you want to turn all ``reload`` settings (every setting that starts -with ``reload_``). on in one fell swoop, you can use +with ``reload_``) on in one fell swoop, you can use ``PYRAMID_RELOAD_ALL=1`` as an environment variable setting or you may use ``reload_all=true`` in the config file. Note that this does not affect settings that do not start with ``reload_*`` such as @@ -341,10 +341,10 @@ affect settings that do not start with ``reload_*`` such as most useful during development, where you may wish to augment or override the more permanent settings in the configuration file. This is useful because many of the reload and debug settings may - have performance or security (i.e., disclosure) implications + have performance or security (i.e., disclosure) implications that make them undesirable in a production environment. -.. index:: +.. index:: single: reload_templates single: reload_assets @@ -442,7 +442,7 @@ Here's how: registry = pyramid.threadlocal.get_current_registry() settings = registry.settings debug_frobnosticator = settings['debug_frobnosticator'] - - + + diff --git a/docs/narr/extending.rst b/docs/narr/extending.rst index 9c96fd605..f62c7e6bb 100644 --- a/docs/narr/extending.rst +++ b/docs/narr/extending.rst @@ -120,7 +120,7 @@ are declarations made using the :meth:`pyramid.config.Configurator.add_view` method. Assets are files that are accessed by :app:`Pyramid` using the :term:`pkg_resources` API such as static files and templates via a :term:`asset specification`. Other directives and -configurator methods also deal in routes, views, and assets. For example, +configurator methods also deal in routes, views, and assets. For example, the ``add_handler`` directive of the ``pyramid_handlers`` package adds a single route, and some number of views. @@ -163,7 +163,7 @@ views or routes which performs overrides. if __name__ == '__main__': config.scan('someotherpackage') config.commit() - config.add_view('mypackage.views.myview', name='myview' + config.add_view('mypackage.views.myview', name='myview') Once this is done, you should be able to extend or override the application like any other (see :ref:`extending_the_application`). @@ -201,7 +201,7 @@ like this: application (e.g. ``python setup.py develop`` or ``python setup.py install``). -- Change the ``main`` function in the new package's ``__init__py`` to include +- Change the ``main`` function in the new package's ``__init__.py`` to include the original :app:`Pyramid` application's configuration functions via :meth:`pyramid.config.Configurator.include` statements or a :term:`scan`. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 7e3fe0a5c..be139ad74 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -84,7 +84,7 @@ The default forbidden response has a 403 status code and is very plain, but the view which generates it can be overridden as necessary. The :term:`forbidden view` callable is a view callable like any other. The -:term:`view configuration` which causes it to be a "not found" view consists +:term:`view configuration` which causes it to be a "forbidden" view consists only of naming the :exc:`pyramid.exceptions.Forbidden` class as the ``context`` of the view configuration. @@ -182,7 +182,7 @@ Adding Renderer Globals ----------------------- Whenever :app:`Pyramid` handles a request to perform a rendering (after a -view with a ``renderer=`` configuration attribute is invoked, or when the any +view with a ``renderer=`` configuration attribute is invoked, or when any of the methods beginning with ``render`` within the :mod:`pyramid.renderers` module are called), *renderer globals* can be injected into the *system* values sent to the renderer. By default, no renderer globals are injected, @@ -199,7 +199,7 @@ callable object or a :term:`dotted Python name` representing such a callable. :linenos: def renderer_globals_factory(system): - return {'a':1} + return {'a': 1} config = Configurator( renderer_globals_factory=renderer_globals_factory) @@ -220,7 +220,7 @@ already constructed a :term:`configurator` it can also be registered via the from pyramid.config import Configurator def renderer_globals_factory(system): - return {'a':1} + return {'a': 1} config = Configurator() config.set_renderer_globals_factory(renderer_globals_factory) @@ -237,8 +237,8 @@ Using The Before Render Event ----------------------------- Subscribers to the :class:`pyramid.events.BeforeRender` event may introspect -the and modify the set of :term:`renderer globals` before they are passed to -a :term:`renderer`. This event object iself has a dictionary-like interface +and modify the set of :term:`renderer globals` before they are passed to a +:term:`renderer`. This event object iself has a dictionary-like interface that can be used for this purpose. For example: .. code-block:: python @@ -484,7 +484,7 @@ resource by adding a registerAdapter call for from myapp.traversal import URLGenerator from myapp.resources import MyRoot - config.registry.registerAdapter(URLGenerator, (MyRoot, Interface), + config.registry.registerAdapter(URLGenerator, (MyRoot, Interface), IContextURL) In the above example, the ``myapp.traversal.URLGenerator`` class will be used @@ -531,7 +531,7 @@ Using a View Mapper The default calling conventions for view callables are documented in the :ref:`views_chapter` chapter. You can change the way users define view -callbles by employing a :term:`view mapper`. +callables by employing a :term:`view mapper`. A view mapper is an object that accepts a set of keyword arguments and which returns a callable. The returned callable is called with the :term:`view @@ -645,24 +645,22 @@ follows: :linenos: import venusian - from pyramid.threadlocal import get_current_registry from mypackage.interfaces import IMyUtility - + class registerFunction(object): - + def __init__(self, path): self.path = path def register(self, scanner, name, wrapped): registry = scanner.config.registry registry.getUtility(IMyUtility).register( - self.path, wrapped - ) - + self.path, wrapped) + def __call__(self, wrapped): venusian.attach(wrapped, self.register) return wrapped - + This decorator could then be used to register functions throughout your code: @@ -681,16 +679,17 @@ performed, enabling you to set up the utility in advance: from paste.httpserver import serve from pyramid.config import Configurator + from mypackage.interfaces import IMyUtility class UtilityImplementation: - implements(ISomething) + implements(IMyUtility) def __init__(self): self.registrations = {} - def register(self,path,callable_): - self.registrations[path]=callable_ + def register(self, path, callable_): + self.registrations[path] = callable_ if __name__ == '__main__': config = Configurator() diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index e928c6efb..c21a19b5b 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -95,7 +95,7 @@ translations of the same msgid, in case they conflict. :linenos: from pyramid.i18n import TranslationString - ts = TranslationString('Add ${number}', mapping={'number':1}, + ts = TranslationString('Add ${number}', mapping={'number':1}, domain='form') The above translation string named a domain of ``form``. A @@ -170,7 +170,7 @@ to: :linenos: from pyramid.i18n import TranslationString as _ - ts = _('Add ${number}', msgid='add-number', mapping={'number':1}, + ts = _('Add ${number}', msgid='add-number', mapping={'number':1}, domain='pyramid') You can set up your own translation string factory much like the one @@ -231,7 +231,7 @@ GNU gettext uses three types of files in the translation framework, A ``.pot`` file is created by a program which searches through your project's source code and which picks out every :term:`message - identifier` passed to one of the '``_()`` functions + identifier` passed to one of the ``_()`` functions (eg. :term:`translation string` constructions). The list of all message identifiers is placed into a ``.pot`` file, which serves as a template for creating ``.po`` files. @@ -288,7 +288,7 @@ like so: .. code-block:: text C> cd \my\virtualenv - C> bin\easy_install Babel lingua + C> Scripts\easy_install Babel lingua .. index:: single: Babel; message extractors @@ -535,7 +535,7 @@ translation in a view component of an application might look like so: from pyramid.i18n import get_localizer from pyramid.i18n import TranslationString - ts = TranslationString('Add ${number}', mapping={'number':1}, + ts = TranslationString('Add ${number}', mapping={'number':1}, domain='pyramid') def aview(request): @@ -844,7 +844,7 @@ Then as a part of the code of a custom :term:`locale negotiator`: .. code-block:: python :linenos: - + from pyramid.threadlocal import get_current_registry settings = get_current_registry().settings languages = settings['available_languages'].split() @@ -897,7 +897,7 @@ application startup. For example: :linenos: from pyramid.config import Configurator - config.add_translation_dirs('my.application:locale/', + config.add_translation_dirs('my.application:locale/', 'another.application:locale/') A message catalog in a translation directory added via diff --git a/docs/narr/zca.rst b/docs/narr/zca.rst index 19c52d0c9..a99fd8b24 100644 --- a/docs/narr/zca.rst +++ b/docs/narr/zca.rst @@ -38,10 +38,10 @@ code is high. While the ZCA is an excellent tool with which to build a *framework* such as :app:`Pyramid`, it is not always the best tool with which to build an *application* due to the opacity of the ``zope.component`` -APIs. Accordingly, :app:`Pyramid` tends to hide the the presence -of the ZCA from application developers. You needn't understand the -ZCA to create a :app:`Pyramid` application; its use is effectively -only a framework implementation detail. +APIs. Accordingly, :app:`Pyramid` tends to hide the presence of the +ZCA from application developers. You needn't understand the ZCA to +create a :app:`Pyramid` application; its use is effectively only a +framework implementation detail. However, developers who are already used to writing :term:`Zope` applications often still wish to use the ZCA while building a diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index 523aef8a8..6e3e4ce37 100644 --- a/docs/tutorials/modwsgi/index.rst +++ b/docs/tutorials/modwsgi/index.rst @@ -109,7 +109,7 @@ commands and files. <Directory /Users/chrism/modwsgi/env> WSGIProcessGroup pyramid - Order allow, deny + Order allow,deny Allow from all </Directory> diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 8781325d2..358c1d5eb 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -64,7 +64,7 @@ Adding ``security.py`` ~~~~~~~~~~~~~~~~~~~~~~ Add a ``security.py`` module within your package (in the same -directory as ``__init__.py``, ``views.py``, etc) with the following +directory as ``__init__.py``, ``views.py``, etc.) with the following content: .. literalinclude:: src/authorization/tutorial/security.py @@ -172,7 +172,7 @@ into its template. We'll add something like this to each view body: logged_in = authenticated_userid(request) We'll then change the return value of each view that has an associated -``renderer`` to pass the `resulting `logged_in`` value to the +``renderer`` to pass the resulting ``logged_in`` value to the template. For example: .. ignore-next-block @@ -291,7 +291,7 @@ as follows: credentials with the username ``editor``, password ``editor`` will show the edit page form being displayed. -- After logging in (as a result of hitting an edit or add page and +- 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/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index b6c083bbf..ae4fa6ffb 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -23,7 +23,7 @@ assumed to return a :term:`response` object. the request as a single argument, you can obtain it via ``request.context``. -We're going to define several :term:`view callable` functions then wire them +We're going to define several :term:`view callable` functions, then wire them into :app:`Pyramid` using some :term:`view configuration`. The source code for this tutorial stage can be browsed via @@ -202,8 +202,8 @@ the form post view callable for the form it renders. The ``context`` of the If the view execution is *not* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``False``), the view -simply renders the edit form, passing the request, the page resource, and a -save_url which will be used as the action of the generated form. +simply renders the edit form, passing the page resource, and a ``save_url`` +which will be used as the action of the generated form. If the view execution *is* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``True``), the view grabs the diff --git a/docs/tutorials/wiki/index.rst b/docs/tutorials/wiki/index.rst index c984c4f01..3edc6ba04 100644 --- a/docs/tutorials/wiki/index.rst +++ b/docs/tutorials/wiki/index.rst @@ -11,8 +11,8 @@ authentication. For cut and paste purposes, the source code for all stages of this tutorial can be browsed at -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki>`_. +`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/ +<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/>`_. .. toctree:: :maxdepth: 2 diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index f4fb4323c..30fb67441 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -122,7 +122,7 @@ Preparation, Windows .. code-block:: text - c:\pyramidtut> Scripts\easy_install docutils repoze.tm2 \ + c:\pyramidtut> Scripts\easy_install docutils repoze.tm2 ^ repoze.zodbconn nose coverage .. _making_a_project: @@ -234,7 +234,7 @@ On Windows: .. code-block:: text - c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial \ + c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial ^ --cover-erase --with-coverage Looks like the code in the ``pyramid_zodb`` scaffold for ZODB projects is diff --git a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py index 3e9266754..f7dab5f47 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py @@ -16,8 +16,8 @@ def main(global_config, **settings): authn_policy = AuthTktAuthenticationPolicy(secret='sosecret', callback=groupfinder) authz_policy = ACLAuthorizationPolicy() - zodb_uri = settings.get('zodb_uri') - if zodb_uri is None: + zodb_uri = settings.get('zodb_uri', False) + if zodb_uri is False: raise ValueError("No 'zodb_uri' in application configuration.") finder = PersistentApplicationFinder(zodb_uri, appmaker) diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py index a9f776980..6a4093a3b 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py @@ -5,8 +5,8 @@ from tutorial.models import appmaker def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - zodb_uri = settings.get('zodb_uri') - if zodb_uri is None: + zodb_uri = settings.get('zodb_uri', False) + if zodb_uri is False: raise ValueError("No 'zodb_uri' in application configuration.") finder = PersistentApplicationFinder(zodb_uri, appmaker) diff --git a/docs/tutorials/wiki/src/models/tutorial/__init__.py b/docs/tutorials/wiki/src/models/tutorial/__init__.py index bf0f683bf..73fc81d23 100644 --- a/docs/tutorials/wiki/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/models/tutorial/__init__.py @@ -5,8 +5,8 @@ from tutorial.models import appmaker def main(global_config, **settings): """ This function returns a WSGI application. """ - zodb_uri = settings.get('zodb_uri') - if zodb_uri is None: + zodb_uri = settings.get('zodb_uri', False) + if zodb_uri is False: raise ValueError("No 'zodb_uri' in application configuration.") finder = PersistentApplicationFinder(zodb_uri, appmaker) diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py index d9ff866f1..0ce5ea718 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py @@ -139,16 +139,20 @@ class FunctionalTests(unittest.TestCase): self.tmpdir = tempfile.mkdtemp() dbpath = os.path.join( self.tmpdir, 'test.db') - settings = { 'zodb_uri' : 'file://' + dbpath } + from repoze.zodbconn.uri import db_from_uri + db = db_from_uri('file://' + dbpath) + settings = { 'zodb_uri' : None } app = main({}, **settings) - from repoze.zodbconn.middleware import EnvironmentDeleterMiddleware - app = EnvironmentDeleterMiddleware(app) + from repoze.zodbconn.connector import Connector + app = Connector(app, db) + self.db = db from webtest import TestApp self.testapp = TestApp(app) def tearDown(self): import shutil + self.db.close() shutil.rmtree( self.tmpdir ) def test_root(self): diff --git a/docs/tutorials/wiki/src/views/tutorial/__init__.py b/docs/tutorials/wiki/src/views/tutorial/__init__.py index 91f7c2624..04a01fead 100644 --- a/docs/tutorials/wiki/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/views/tutorial/__init__.py @@ -5,8 +5,8 @@ from tutorial.models import appmaker def main(global_config, **settings): """ This function returns a WSGI application. """ - zodb_uri = settings.get('zodb_uri') - if zodb_uri is None: + zodb_uri = settings.get('zodb_uri', False) + if zodb_uri is False: raise ValueError("No 'zodb_uri' in application configuration.") finder = PersistentApplicationFinder(zodb_uri, appmaker) diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst index f3151dbcc..c843a0129 100644 --- a/docs/tutorials/wiki/tests.rst +++ b/docs/tutorials/wiki/tests.rst @@ -73,6 +73,6 @@ The expected result looks something like: ......... ---------------------------------------------------------------------- - Ran 9 tests in 0.203s + Ran 23 tests in 1.653s OK diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 64c587f07..76ce4b83f 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -76,7 +76,7 @@ information about what an :term:`ACL` represents. :meth:`pyramid.config.Configurator.add_route` for more info. We'll pass the ``RootFactory`` we created in the step above in as the -``root_factory`` argument to a :term:`Configurator`. +``root_factory`` argument to a :term:`Configurator`. Configuring an Authorization Policy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -116,7 +116,7 @@ We'll also change ``__init__.py``, adding a call to :term:`view callable`. This is also known as a :term:`forbidden view`: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 41-43 + :lines: 25,41-43 :linenos: :language: python @@ -163,7 +163,7 @@ Adding ``security.py`` ---------------------- Add a ``security.py`` module within your package (in the same directory as -:file:`__init__.py`, :file:`views.py`, etc) with the following content: +:file:`__init__.py`, :file:`views.py`, etc.) with the following content: .. literalinclude:: src/authorization/tutorial/security.py :linenos: diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 82e112c64..6151e0e25 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -92,7 +92,7 @@ use :meth:`pyramid.config.Configurator.add_view` in :term:`URL dispatch` to register views for the routes, mapping your patterns to code: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 14 + :lines: 14-15 :language: py The first positional ``add_view`` argument ``tutorial.views.my_view`` is the @@ -102,7 +102,7 @@ which returns a response or a dictionary. This view also names a ``renderer``, which is a template which lives in the ``templates`` subdirectory of the package. When the ``tutorial.views.my_view`` view returns a dictionary, a :term:`renderer` will use this template to create a -response. This +response. Finally, we use the :meth:`pyramid.config.Configurator.make_wsgi_app` method to return a :term:`WSGI` application: @@ -133,7 +133,7 @@ Let's take a look. First, we need some imports to support later code. :linenos: :language: py -Next we set up a SQLAlchemy "DBSession" object: +Next we set up a SQLAlchemy "DBSession" object: .. literalinclude:: src/basiclayout/tutorial/models.py :lines: 15-16 diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index e5d283125..7aa2214fc 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -45,7 +45,7 @@ SQLAlchemy models are easier to use than directly-mapped ones. :language: python As you can see, our ``Page`` class has a class level attribute -``__tablename__`` which equals the string ``pages``. This means that +``__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.Column`). These will map to @@ -67,7 +67,7 @@ Here, we're using a slightly different binding syntax. It is otherwise largely the same as the ``initialize_sql`` in the paster-generated ``models.py``. -Our DBSession assignment stays the same as the original generated +Our ``DBSession`` assignment stays the same as the original generated ``models.py``. Looking at the Result of all Our Edits to ``models.py`` diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 832f90b92..cea376b77 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -25,9 +25,9 @@ match has an attribute named ``matchdict`` that contains the elements placed into the URL by the ``pattern`` of a ``route`` statement. For instance, if a call to :meth:`pyramid.config.Configurator.add_route` in ``__init__.py`` had the pattern ``{one}/{two}``, and the URL at ``http://example.com/foo/bar`` -was invoked, matching this pattern, the matchdict dictionary attached to the -request passed to the view would have a ``one`` key with the value ``foo`` -and a ``two`` key with the value ``bar``. +was invoked, matching this pattern, the ``matchdict`` dictionary attached to +the request passed to the view would have a ``'one'`` key with the value +``'foo'`` and a ``'two'`` key with the value ``'bar'``. The source code for this tutorial stage can be browsed at `http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/views/ @@ -80,9 +80,9 @@ to be edited. We'll describe each one briefly and show the resulting The ``view_wiki`` view function ------------------------------- -The ``view_wiki`` function will respond as the :term:`default view` of a -``Wiki`` model object. It always redirects to a URL which represents the -path to our "FrontPage". +The ``view_wiki`` function is the :term:`default view` that will be called +when a request is made to the root URL of our wiki. It always redirects to +a URL which represents the path to our "FrontPage". .. literalinclude:: src/views/tutorial/views.py :pyobject: view_wiki @@ -99,11 +99,10 @@ page (e.g. ``http://localhost:6543/FrontPage``), and will use it as the The ``view_page`` view function ------------------------------- -The ``view_page`` function will respond as the :term:`default view` of -a ``Page`` object. The ``view_page`` function renders the -:term:`ReStructuredText` body of a page (stored as the ``data`` -attribute of a Page object) as HTML. Then it substitutes an HTML -anchor for each *WikiWord* reference in the rendered HTML using a +The ``view_page`` function will be used to show a single page of our +wiki. It renders the :term:`ReStructuredText` body of a page (stored as +the ``data`` attribute of a Page object) as HTML. Then it substitutes an +HTML anchor for each *WikiWord* reference in the rendered HTML using a compiled regular expression. .. literalinclude:: src/views/tutorial/views.py @@ -146,15 +145,15 @@ will have the values we need to construct URLs and find model objects. :linenos: :language: python -The matchdict will have a ``pagename`` key that matches the name of the page -we'd like to add. If our add view is invoked via, -e.g. ``http://localhost:6543/add_page/SomeName``, the ``pagename`` value in -the matchdict will be ``SomeName``. +The ``matchdict`` will have a ``'pagename'`` key that matches the name of +the page we'd like to add. If our add view is invoked via, +e.g. ``http://localhost:6543/add_page/SomeName``, the value for +``'pagename'`` in the ``matchdict`` will be ``'SomeName'``. If the view execution is *not* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``False``), the view callable renders a template. To do so, it generates a "save url" which the -template use as the form post URL during rendering. We're lazy here, so +template uses as the form post URL during rendering. We're lazy here, so we're trying to use the same template (``templates/edit.pt``) for the add view as well as the page edit view, so we create a dummy Page object in order to satisfy the edit form's desire to have *some* page object exposed as @@ -163,10 +162,10 @@ view to a response. If the view execution *is* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``True``), we scrape the page body -from the form data, create a Page object using the name in the matchdict -``pagename``, and obtain the page body from the request, and save it into the -database using ``session.add``. We then redirect back to the ``view_page`` -view (the :term:`default view` for a Page) for the newly created page. +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 +``session.add``. We then redirect back to the ``view_page`` view for the +newly created page. The ``edit_page`` view function ------------------------------- @@ -174,7 +173,7 @@ The ``edit_page`` view function The ``edit_page`` function will be invoked when a user clicks the "Edit this Page" button on the view form. It renders an edit form but it also acts as the handler for the form it renders. The ``matchdict`` attribute of the -request passed to the ``edit_page`` view will have a ``pagename`` key +request passed to the ``edit_page`` view will have a ``'pagename'`` key matching the name of the page the user wants to edit. .. literalinclude:: src/views/tutorial/views.py @@ -184,14 +183,14 @@ matching the name of the page the user wants to edit. If the view execution is *not* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``False``), the view -simply renders the edit form, passing the request, the page object, and a -save_url which will be used as the action of the generated form. +simply renders the edit form, passing the page object and a ``save_url`` +which will be used as the action of the generated form. If the view execution *is* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``True``), the view grabs the -``body`` element of the request parameter and sets it as the ``data`` -attribute of the page object. It then redirects to the default view of the -wiki page, which will always be the ``view_page`` view. +``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. Viewing the Result of all Our Edits to ``views.py`` =================================================== @@ -274,7 +273,7 @@ Mapping Views to URLs in ``__init__.py`` The ``__init__.py`` file contains :meth:`pyramid.config.Configurator.add_view` calls which serve to map routes via :term:`url dispatch` to views. First, we’ll get rid of the -existing route created by the template using the name ``home``. It’s only an +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 @@ -282,7 +281,7 @@ 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``. + to the route named ``view_wiki``. #. Add a declaration which maps the pattern ``/{pagename}`` to the route named ``view_page``. This is the regular view for a page. @@ -342,46 +341,3 @@ Exception('Forced Exception')``). Then visit the error-raising view in a browser. You should see an interactive exception handler in the browser which allows you to examine values in a post-mortem mode. -Adding Tests -============ - -Since we've added a good bit of imperative code here, it's useful to -define tests for the views we've created. We'll change our tests.py -module to look like this: - -.. literalinclude:: src/views/tutorial/tests.py - :linenos: - :language: python - -We can then run the tests using something like: - -.. code-block:: text - :linenos: - - $ python setup.py test -q - -The expected output is something like: - -.. code-block:: text - :linenos: - - running test - running egg_info - writing requirements to tutorial.egg-info/requires.txt - writing tutorial.egg-info/PKG-INFO - writing top-level names to tutorial.egg-info/top_level.txt - writing dependency_links to tutorial.egg-info/dependency_links.txt - writing entry points to tutorial.egg-info/entry_points.txt - unrecognized .svn/entries format in - reading manifest file 'tutorial.egg-info/SOURCES.txt' - writing manifest file 'tutorial.egg-info/SOURCES.txt' - running build_ext - ...... - ---------------------------------------------------------------------- - Ran 6 tests in 0.181s - - OK - - - - diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 1aff949b9..d05d70f3c 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -11,8 +11,8 @@ basic Wiki application with authentication. For cut and paste purposes, the source code for all stages of this tutorial can be browsed at -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/>`_. +`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/ +<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/>`_. .. toctree:: :maxdepth: 2 @@ -23,6 +23,7 @@ tutorial can be browsed at definingmodels definingviews authorization + tests distributing diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 5f5b0c216..bd597b5df 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -73,7 +73,7 @@ Preparation, Windows .. code-block:: text - c:\pyramidtut> Scripts\easy_install docutils \ + c:\pyramidtut> Scripts\easy_install docutils ^ nose coverage zope.sqlalchemy SQLAlchemy repoze.tm2 @@ -205,7 +205,7 @@ On Windows: .. code-block:: text - c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial \ + c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial ^ --cover-erase --with-coverage Looks like our package's ``models`` module doesn't quite have 100% diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models.py b/docs/tutorials/wiki2/src/authorization/tutorial/models.py index 487299c4c..53c6d1122 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models.py @@ -26,16 +26,17 @@ class Page(Base): data = Column(Text) def __init__(self, name, data): - self.name = name - self.data = data + self.name = name + self.data = data def initialize_sql(engine): DBSession.configure(bind=engine) Base.metadata.bind = engine Base.metadata.create_all(engine) try: + transaction.begin() session = DBSession() - page = Page('FrontPage', 'initial data') + page = Page('FrontPage', 'This is the front page') session.add(page) transaction.commit() except IntegrityError: diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index 5abd8391e..e0b84971d 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -2,7 +2,7 @@ import re from docutils.core import publish_parts -from pyramid.httpexceptions import HTTPFound +from pyramid.httpexceptions import HTTPFound, HTTPNotFound from pyramid.security import authenticated_userid from pyramid.url import route_url @@ -19,7 +19,9 @@ def view_wiki(request): def view_page(request): pagename = request.matchdict['pagename'] session = DBSession() - page = session.query(Page).filter_by(name=pagename).one() + page = session.query(Page).filter_by(name=pagename).first() + if page is None: + return HTTPNotFound('No such page') def check(match): word = match.group(1) @@ -51,7 +53,7 @@ def add_page(request): page = Page('', '') logged_in = authenticated_userid(request) return dict(page=page, save_url=save_url, logged_in=logged_in) - + def edit_page(request): name = request.matchdict['pagename'] session = DBSession() diff --git a/docs/tutorials/wiki2/src/models/tutorial/models.py b/docs/tutorials/wiki2/src/models/tutorial/models.py index 797fff929..ecc8d567b 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models.py @@ -24,16 +24,17 @@ class Page(Base): data = Column(Text) def __init__(self, name, data): - self.name = name - self.data = data + self.name = name + self.data = data def initialize_sql(engine): DBSession.configure(bind=engine) Base.metadata.bind = engine Base.metadata.create_all(engine) try: + transaction.begin() session = DBSession() - page = Page('FrontPage', 'initial data') + page = Page('FrontPage', 'This is the front page') session.add(page) transaction.commit() except IntegrityError: diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py new file mode 100644 index 000000000..98a4969e9 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests.py @@ -0,0 +1,266 @@ +import unittest + +from pyramid import testing + + +def _initTestingDB(): + from tutorial.models import DBSession + from tutorial.models import Base + from sqlalchemy import create_engine + engine = create_engine('sqlite:///:memory:') + DBSession.configure(bind=engine) + Base.metadata.bind = engine + Base.metadata.create_all(engine) + return DBSession + +def _registerRoutes(config): + config.add_route('view_page', '{pagename}') + config.add_route('edit_page', '{pagename}/edit_page') + config.add_route('add_page', 'add_page/{pagename}') + + +class PageModelTests(unittest.TestCase): + + def setUp(self): + self.session = _initTestingDB() + + def tearDown(self): + self.session.remove() + + def _getTargetClass(self): + from tutorial.models import Page + return Page + + def _makeOne(self, name='SomeName', data='some data'): + return self._getTargetClass()(name, data) + + def test_constructor(self): + instance = self._makeOne() + self.assertEqual(instance.name, 'SomeName') + self.assertEqual(instance.data, 'some data') + +class InitializeSqlTests(unittest.TestCase): + + def setUp(self): + from tutorial.models import DBSession + DBSession.remove() + + def tearDown(self): + from tutorial.models import DBSession + DBSession.remove() + + def _callFUT(self, engine): + from tutorial.models import initialize_sql + return initialize_sql(engine) + + def test_it(self): + from sqlalchemy import create_engine + engine = create_engine('sqlite:///:memory:') + self._callFUT(engine) + from tutorial.models import DBSession, Page + self.assertEqual(DBSession.query(Page).one().data, + 'This is the front page') + +class ViewWikiTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views import view_wiki + return view_wiki(request) + + def test_it(self): + _registerRoutes(self.config) + request = testing.DummyRequest() + response = self._callFUT(request) + self.assertEqual(response.location, 'http://example.com/FrontPage') + +class ViewPageTests(unittest.TestCase): + def setUp(self): + self.session = _initTestingDB() + self.config = testing.setUp() + + def tearDown(self): + self.session.remove() + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views import view_page + return view_page(request) + + def test_it(self): + from tutorial.models import Page + request = testing.DummyRequest() + request.matchdict['pagename'] = 'IDoExist' + page = Page('IDoExist', 'Hello CruelWorld IDoExist') + self.session.add(page) + _registerRoutes(self.config) + info = self._callFUT(request) + self.assertEqual(info['page'], page) + self.assertEqual( + info['content'], + '<div class="document">\n' + '<p>Hello <a href="http://example.com/add_page/CruelWorld">' + 'CruelWorld</a> ' + '<a href="http://example.com/IDoExist">' + 'IDoExist</a>' + '</p>\n</div>\n') + self.assertEqual(info['edit_url'], + 'http://example.com/IDoExist/edit_page') + +class AddPageTests(unittest.TestCase): + def setUp(self): + self.session = _initTestingDB() + self.config = testing.setUp() + self.config.begin() + + def tearDown(self): + self.session.remove() + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views import add_page + return add_page(request) + + def test_it_notsubmitted(self): + _registerRoutes(self.config) + request = testing.DummyRequest() + request.matchdict = {'pagename':'AnotherPage'} + info = self._callFUT(request) + self.assertEqual(info['page'].data,'') + self.assertEqual(info['save_url'], + 'http://example.com/add_page/AnotherPage') + + def test_it_submitted(self): + from tutorial.models import Page + _registerRoutes(self.config) + request = testing.DummyRequest({'form.submitted':True, + 'body':'Hello yo!'}) + request.matchdict = {'pagename':'AnotherPage'} + self._callFUT(request) + page = self.session.query(Page).filter_by(name='AnotherPage').one() + self.assertEqual(page.data, 'Hello yo!') + +class EditPageTests(unittest.TestCase): + def setUp(self): + self.session = _initTestingDB() + self.config = testing.setUp() + + def tearDown(self): + self.session.remove() + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views import edit_page + return edit_page(request) + + def test_it_notsubmitted(self): + from tutorial.models import Page + _registerRoutes(self.config) + request = testing.DummyRequest() + request.matchdict = {'pagename':'abc'} + page = Page('abc', 'hello') + self.session.add(page) + info = self._callFUT(request) + self.assertEqual(info['page'], page) + self.assertEqual(info['save_url'], + 'http://example.com/abc/edit_page') + + def test_it_submitted(self): + from tutorial.models import Page + _registerRoutes(self.config) + request = testing.DummyRequest({'form.submitted':True, + 'body':'Hello yo!'}) + request.matchdict = {'pagename':'abc'} + page = Page('abc', 'hello') + self.session.add(page) + response = self._callFUT(request) + self.assertEqual(response.location, 'http://example.com/abc') + self.assertEqual(page.data, 'Hello yo!') + +class FunctionalTests(unittest.TestCase): + + viewer_login = '/login?login=viewer&password=viewer' \ + '&came_from=FrontPage&form.submitted=Login' + viewer_wrong_login = '/login?login=viewer&password=incorrect' \ + '&came_from=FrontPage&form.submitted=Login' + editor_login = '/login?login=editor&password=editor' \ + '&came_from=FrontPage&form.submitted=Login' + + def setUp(self): + from tutorial import main + settings = { 'sqlalchemy.url': 'sqlite:///:memory:'} + app = main({}, **settings) + from webtest import TestApp + self.testapp = TestApp(app) + + def tearDown(self): + del self.testapp + from tutorial.models import DBSession + DBSession.remove() + + def test_root(self): + res = self.testapp.get('/', status=302) + self.assertTrue(not res.body) + + def test_FrontPage(self): + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue('FrontPage' in res.body) + + def test_unexisting_page(self): + res = self.testapp.get('/SomePage', status=404) + + def test_successful_log_in(self): + res = self.testapp.get(self.viewer_login, status=302) + self.assertTrue(res.location == 'FrontPage') + + def test_failed_log_in(self): + res = self.testapp.get(self.viewer_wrong_login, status=200) + self.assertTrue('login' in res.body) + + def test_logout_link_present_when_logged_in(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue('Logout' in res.body) + + def test_logout_link_not_present_after_logged_out(self): + self.testapp.get(self.viewer_login, status=302) + self.testapp.get('/FrontPage', status=200) + res = self.testapp.get('/logout', status=302) + self.assertTrue('Logout' not in res.body) + + def test_anonymous_user_cannot_edit(self): + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue('Login' in res.body) + + def test_anonymous_user_cannot_add(self): + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue('Login' in res.body) + + def test_viewer_user_cannot_edit(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue('Login' in res.body) + + def test_viewer_user_cannot_add(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue('Login' in res.body) + + def test_editors_member_user_can_edit(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue('Editing' in res.body) + + def test_editors_member_user_can_add(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue('Editing' in res.body) + + def test_editors_member_user_can_view(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue('FrontPage' in res.body) diff --git a/docs/tutorials/wiki2/src/views/tutorial/models.py b/docs/tutorials/wiki2/src/views/tutorial/models.py index 23b8afab8..960c14941 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models.py @@ -23,14 +23,15 @@ class Page(Base): data = Column(Text) def __init__(self, name, data): - self.name = name - self.data = data + self.name = name + self.data = data def initialize_sql(engine): DBSession.configure(bind=engine) Base.metadata.bind = engine Base.metadata.create_all(engine) try: + transaction.begin() session = DBSession() page = Page('FrontPage', 'initial data') session.add(page) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index b8896abe7..f3d7f4a99 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -2,7 +2,7 @@ import re from docutils.core import publish_parts -from pyramid.httpexceptions import HTTPFound +from pyramid.httpexceptions import HTTPFound, HTTPNotFound from pyramid.url import route_url from tutorial.models import DBSession @@ -16,9 +16,11 @@ def view_wiki(request): pagename='FrontPage')) def view_page(request): - matchdict = request.matchdict + pagename = request.matchdict['pagename'] session = DBSession() - page = session.query(Page).filter_by(name=matchdict['pagename']).one() + page = session.query(Page).filter_by(name=pagename).first() + if page is None: + return HTTPNotFound('No such page') def check(match): word = match.group(1) @@ -32,8 +34,7 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) - edit_url = route_url('edit_page', request, - pagename=matchdict['pagename']) + edit_url = route_url('edit_page', request, pagename=pagename) return dict(page=page, content=content, edit_url=edit_url) def add_page(request): @@ -48,7 +49,7 @@ def add_page(request): save_url = route_url('add_page', request, pagename=name) page = Page('', '') return dict(page=page, save_url=save_url) - + def edit_page(request): name = request.matchdict['pagename'] session = DBSession() diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst new file mode 100644 index 000000000..7a4e65529 --- /dev/null +++ b/docs/tutorials/wiki2/tests.rst @@ -0,0 +1,74 @@ +============ +Adding Tests +============ + +We will now add tests for the models and the views and a few functional +tests in the ``tests.py``. Tests ensure that an application works, and +that it continues to work after some changes are made in the future. + +Testing the Models +================== + +We write a test class for the model class ``Page`` and another test class +for the ``initialize_sql`` function. + +To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a +result of the ``pyramid_routesalchemy`` project generator. We'll add two +test classes: one for the ``Page`` model named ``PageModelTests``, one for the +``initialize_sql`` function named ``InitializeSqlTests``. + +Testing the Views +================= + +We'll modify our ``tests.py`` file, adding tests for each view function we +added above. As a result, we'll *delete* the ``ViewTests`` test in the file, +and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``, +``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``, +``view_page``, ``add_page``, and ``edit_page`` views respectively. + +Functional tests +================ + +We test the whole application, covering security aspects that are not +tested in the unit tests, like logging in, logging out, checking that +the ``viewer`` user cannot add or edit pages, but the ``editor`` user +can, and so on. + +Viewing the results of all our edits to ``tests.py`` +==================================================== + +Once we're done with the ``tests.py`` module, it will look a lot like the +below: + +.. literalinclude:: src/tests/tutorial/tests.py + :linenos: + :language: python + +Running the Tests +================= + +We can run these tests by using ``setup.py test`` in the same way we did in +:ref:`running_tests`. Assuming our shell's current working directory is the +"tutorial" distribution directory: + +On UNIX: + +.. code-block:: text + + $ ../bin/python setup.py test -q + +On Windows: + +.. code-block:: text + + c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q + +The expected result looks something like: + +.. code-block:: text + + ...................... + ---------------------------------------------------------------------- + Ran 22 tests in 2.700s + + OK |
