diff options
Diffstat (limited to 'docs')
25 files changed, 812 insertions, 445 deletions
diff --git a/docs/api/request.rst b/docs/api/request.rst index d17441c0a..639d0fcd8 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -85,6 +85,41 @@ of ``request.exception`` will be ``None`` within response and finished callbacks. + .. attribute:: response + + This attribute is actually a "reified" property which returns an + instance of the :class:`pyramid.response.Response` class. The response + object returned does not exist until this attribute is accessed. Once + it is accessed, subsequent accesses to this request object will return + the same :class:`~pyramid.response.Response` object. + + The ``request.response`` API can is used by renderers. A render obtains + the response object it will return from a view that uses that renderer + by accessing ``request.response``. Therefore, it's possible to use the + ``request.response`` API to set up a response object with "the right" + attributes (e.g. by calling ``request.response.set_cookie(...)`` or + ``request.response.content_type = 'text/plain'``, etc) within a view + that uses a renderer. For example, within a view that uses a + :term:`renderer`:: + + response = request.response + response.set_cookie('mycookie', 'mine, all mine!') + return {'text':'Value that will be used by the renderer'} + + Mutations to this response object will be preserved in the response sent + to the client after rendering. + + Non-renderer code can also make use of request.response instead of + creating a response "by hand". For example, in view code:: + + response = request.response + response.body = 'Hello!' + response.content_type = 'text/plain' + return response + + Note that the response in this circumstance is not "global"; it still + must be returned from the view code if a renderer is not used. + .. attribute:: session If a :term:`session factory` has been configured, this attribute @@ -127,10 +162,18 @@ .. attribute:: response_* + .. warning:: As of Pyramid 1.1, assignment to ``response_*`` attrs are + deprecated. Assigning to one will cause a deprecation warning to be + emitted. Instead of assigning ``response_*`` attributes to the + request, use API of the the :attr:`pyramid.request.Request.response` + object (exposed to view code as ``request.response``) to influence + response behavior. + You can set attributes on a :class:`pyramid.request.Request` which will influence the behavor of *rendered* responses (views which use a :term:`renderer` and which don't directly return a response). These attributes begin with ``response_``, such as ``response_headerlist``. If you need to influence response values from a view that uses a renderer (such as the status code, a header, the content type, etc) see, - :ref:`response_request_attrs`. + :ref:`response_prefixed_attrs`. + diff --git a/docs/conf.py b/docs/conf.py index a987106d4..a610351ff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -93,7 +93,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.0' +version = '1.1a0' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 099bce35f..5ee554284 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -295,15 +295,9 @@ These are the methods of the configurator which provide conflict detection: :meth:`~pyramid.config.Configurator.set_locale_negotiator` and :meth:`~pyramid.config.Configurator.set_default_permission`. -Some other methods of the configurator also indirectly provide conflict -detection, because they're implemented in terms of conflict-aware methods: - -- :meth:`~pyramid.config.Configurator.add_route` does a second type of - conflict detection when a ``view`` parameter is passed (it calls - ``add_view``). - -- :meth:`~pyramid.config.Configurator.static_view`, a frontend for - ``add_route`` and ``add_view``. +:meth:`~pyramid.config.Configurator.add_static_view` also indirectly +provides conflict detection, because it's implemented in terms of the +conflict-aware ``add_route`` and ``add_view`` methods. .. _including_configuration: diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index bbb673ecc..8d0e7058c 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -341,7 +341,8 @@ application's startup code. # .. every other add_route declaration should come # before this one, as it will, by default, catch all requests - config.add_route('catchall_static', '/*subpath', 'myapp.static.static_view') + config.add_route('catchall_static', '/*subpath') + config.add_view('myapp.static.static_view', route_name='catchall_static') The special name ``*subpath`` above is used by the :class:`~pyramid.view.static` view callable to signify the path of the file @@ -384,8 +385,8 @@ Or you might register it to be the view callable for a particular route: .. code-block:: python :linenos: - config.add_route('favicon', '/favicon.ico', - view='myapp.views.favicon_view') + config.add_route('favicon', '/favicon.ico') + config.add_view('myapp.views.favicon_view', route_name='favicon') Because this is a simple view callable, it can be protected with a :term:`permission` or can be configured to respond under different diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index 7b7946aae..8c299f3a3 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -381,3 +381,66 @@ around in overridden asset directories. ``reload_assets`` makes the system *very slow* when templates are in use. Never set ``reload_assets`` to ``True`` on a production system. +Adding A Custom Setting +----------------------- + +From time to time, you may need to add a custom setting to your application. +Here's how: + +- If you're using an ``.ini`` file, change the ``.ini`` file, adding the + setting to the ``[app:foo]`` section representing your Pyramid application. + For example: + + .. code-block:: ini + + [app:myapp] + # .. other settings + debug_frobnosticator = True + +- In the ``main()`` function that represents the place that your Pyramid WSGI + application is created, anticipate that you'll be getting this key/value + pair as a setting and do any type conversion necessary. + + If you've done any type conversion of your custom value, reset the + converted values into the ``settings`` dictionary *before* you pass the + dictionary as ``settings`` to the :term:`Configurator`. For example: + + .. code-block:: python + + def main(global_config, **settings): + # ... + from pyramid.settings import asbool + debug_frobnosticator = asbool(settings.get( + 'debug_frobnosticator', 'false')) + settings['debug_frobnosticator'] = debug_frobnosticator + config = Configurator(settings=settings) + + .. note:: It's especially important that you mutate the ``settings`` + dictionary with the converted version of the variable *before* passing + it to the Configurator: the configurator makes a *copy* of ``settings``, + it doesn't use the one you pass directly. + +- In the runtime code that you need to access the new settings value, find + the value in the ``registry.settings`` dictionary and use it. In + :term:`view` code (or any other code that has access to the request), the + easiest way to do this is via ``request.registry.settings``. For example: + + .. code-block:: python + + registry = request.registry.settings + debug_frobnosticator = settings['debug_frobnosticator'] + + If you wish to use the value in code that does not have access to the + request and you wish to use the value, you'll need to use the + :func:`pyramid.threadlocal.get_current_registry` API to obtain the current + registry, then ask for its ``settings`` attribute. For example: + + .. code-block:: python + + registry = pyramid.threadlocal.get_current_registry() + settings = registry.settings + debug_frobnosticator = settings['debug_frobnosticator'] + + + + diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst index 780cb0975..f8ed743fb 100644 --- a/docs/narr/hybrid.rst +++ b/docs/narr/hybrid.rst @@ -33,7 +33,7 @@ URL Dispatch Only ~~~~~~~~~~~~~~~~~ An application that uses :term:`url dispatch` exclusively to map URLs to code -will often have statements like this within your application startup +will often have statements like this within application startup configuration: .. code-block:: python @@ -41,15 +41,20 @@ configuration: # config is an instance of pyramid.config.Configurator - config.add_route('foobar', '{foo}/{bar}', view='myproject.views.foobar') - config.add_route('bazbuz', '{baz}/{buz}', view='myproject.views.bazbuz') + config.add_route('foobar', '{foo}/{bar}') + config.add_route('bazbuz', '{baz}/{buz}') -Each :term:`route` typically corresponds to a single view callable, -and when that route is matched during a request, the view callable -named by the ``view`` attribute is invoked. + config.add_view('myproject.views.foobar', route_name='foobar') + config.add_view('myproject.views.bazbuz', route_name='bazbuz') -Typically, an application that uses only URL dispatch won't perform any calls -to :meth:`pyramid.config.Configurator.add_view` in its startup code. +Each :term:`route` corresponds to one or more view callables. Each view +callable is associated with a route by passing a ``route_name`` parameter +that matches its name during a call to +:meth:`~pyramid.config.Configurator.add_view`. When a route is matched +during a request, :term:`view lookup` is used to match the request to its +associated view callable. The presence of calls to +:meth:`~pyramid.config.Configurator.add_route` signify that an application is +using URL dispatch. Traversal Only ~~~~~~~~~~~~~~ @@ -196,12 +201,9 @@ remainder becomes the path used to perform traversal. The ``*remainder`` route pattern syntax is explained in more detail within :ref:`route_pattern_syntax`. -Note that unlike the examples provided within :ref:`urldispatch_chapter`, the -``add_route`` configuration statement named previously does not pass a -``view`` argument. This is because a hybrid mode application relies on -:term:`traversal` to do :term:`resource location` and :term:`view lookup` -instead of invariably invoking a specific view callable named directly within -the matched route's configuration. +A hybrid mode application relies more heavily on :term:`traversal` to do +:term:`resource location` and :term:`view lookup` than most examples indicate +within :ref:`urldispatch_chapter`. Because the pattern of the above route ends with ``*traverse``, when this route configuration is matched during a request, :app:`Pyramid` will attempt @@ -426,13 +428,11 @@ attribute. Using ``*subpath`` in a Route Pattern ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -There are certain extremely rare cases when you'd like to influence -the traversal :term:`subpath` when a route matches without actually -performing traversal. For instance, the -:func:`pyramid.wsgi.wsgiapp2` decorator and the -:class:`pyramid.view.static` helper attempt to compute -``PATH_INFO`` from the request's subpath, so it's useful to be able to -influence this value. +There are certain extremely rare cases when you'd like to influence the +traversal :term:`subpath` when a route matches without actually performing +traversal. For instance, the :func:`pyramid.wsgi.wsgiapp2` decorator and the +:class:`pyramid.view.static` helper attempt to compute ``PATH_INFO`` from the +request's subpath, so it's useful to be able to influence this value. When ``*subpath`` exists in a pattern, no path is actually traversed, but the traversal algorithm will return a :term:`subpath` list implied @@ -442,8 +442,8 @@ commonly in route declarations that look like this: .. code-block:: python :linenos: - config.add_route('static', '/static/*subpath', - view='mypackage.views.static_view') + config.add_route('static', '/static/*subpath') + config.add_view('mypackage.views.static_view', route_name='static') Where ``mypackage.views.static_view`` is an instance of :class:`pyramid.view.static`. This effectively tells the static helper to @@ -458,11 +458,16 @@ application. We'll detail them here. Registering a Default View for a Route That Has a ``view`` Attribute ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. warning:: As of :app:`Pyramid` 1.1 this section is slated to be removed in + a later documentation release because the the ability to add views + directly to the :term:`route configuration` by passing a ``view`` argument + to ``add_route`` has been deprecated. + It is an error to provide *both* a ``view`` argument to a :term:`route configuration` *and* a :term:`view configuration` which names a ``route_name`` that has no ``name`` value or the empty ``name`` value. For -example, this pair of declarations will generate a "conflict" error at -startup time. +example, this pair of declarations will generate a conflict error at startup +time. .. code-block:: python :linenos: @@ -490,8 +495,8 @@ Can also be spelled like so: config.add_route('home', '{foo}/{bar}/*traverse') config.add_view('myproject.views.home', route_name='home') -The two spellings are logically equivalent. In fact, the former is -just a syntactical shortcut for the latter. +The two spellings are logically equivalent. In fact, the former is just a +syntactical shortcut for the latter. Binding Extra Views Against a Route Configuration that Doesn't Have a ``*traverse`` Element In Its Pattern ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 0b7cdb834..c3533648b 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -92,8 +92,8 @@ will be employed. return HTTPFound(location='http://example.com') # any renderer avoided Views which use a renderer can vary non-body response attributes (such as -headers and the HTTP status code) by attaching properties to the request. -See :ref:`response_request_attrs`. +headers and the HTTP status code) by attaching a property to the +``request.response`` attribute See :ref:`request_response_attr`. Additional renderers can be added by developers to the system as necessary (see :ref:`adding_and_overriding_renderers`). @@ -147,7 +147,8 @@ representing the ``str()`` serialization of the return value: {'content': 'Hello!'} Views which use the string renderer can vary non-body response attributes by -attaching properties to the request. See :ref:`response_request_attrs`. +using the API of the ``request.response`` attribute. See +:ref:`request_response_attr`. .. index:: pair: renderer; JSON @@ -199,7 +200,8 @@ You can configure a view to use the JSON renderer by naming ``json`` as the Views which use the JSON renderer can vary non-body response attributes by -attaching properties to the request. See :ref:`response_request_attrs`. +using the api of the ``request.response`` attribute. See +:ref:`request_response_attr`. .. index:: pair: renderer; chameleon @@ -269,8 +271,9 @@ Here's an example view configuration which uses a Chameleon text renderer: context='myproject.resources.Hello', renderer='myproject:templates/foo.txt') -Views which use a Chameleon renderer can vary response attributes by -attaching properties to the request. See :ref:`response_request_attrs`. +Views which use a Chameleon renderer can vary response attributes by using +the API of the ``request.response`` attribute. See +:ref:`request_response_attr`. .. index:: pair: renderer; mako @@ -333,7 +336,7 @@ additional :ref:`mako_template_renderer_settings`. single: response headers (from a renderer) single: renderer response headers -.. _response_request_attrs: +.. _request_response_attr: Varying Attributes of Rendered Responses ---------------------------------------- @@ -342,9 +345,43 @@ Before a response constructed by a :term:`renderer` is returned to :app:`Pyramid`, several attributes of the request are examined which have the potential to influence response behavior. -View callables that don't directly return a response should set these -attributes on the ``request`` object via ``setattr`` during their execution, -to influence associated response attributes. +View callables that don't directly return a response should use the API of +the :class:`pyramid.response.Response` attribute available as +``request.response`` during their execution, to influence associated response +behavior. + +For example, if you need to change the response status from within a view +callable that uses a renderer, assign the ``status`` attribute to the +``response`` attribute of the request before returning a result: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(name='gone', renderer='templates/gone.pt') + def myview(request): + request.response.status = '404 Not Found' + return {'URL':request.URL} + +For more information on attributes of the request, see the API documentation +in :ref:`request_module`. For more information on the API of +``request.response``, see :class:`pyramid.response.Response`. + +.. _response_prefixed_attrs: + +Deprecated Mechanism to Vary Attributes of Rendered Responses +------------------------------------------------------------- + +.. warning:: This section describes behavior deprecated in Pyramid 1.1. + +In previous releases of Pyramid (1.0 and before), the ``request.response`` +attribute did not exist. Instead, Pyramid required users to set special +``response_`` -prefixed attributes of the request to influence response +behavior. As of Pyramid 1.1, those request attributes are deprecated and +their use will cause a deprecation warning to be issued when used. Until +their existence is removed completely, we document them below, for benefit of +people with older code bases. ``response_content_type`` Defines the content-type of the resulting response, @@ -367,23 +404,6 @@ to influence associated response attributes. returning various values in the ``response_headerlist``, this is purely a convenience. -For example, if you need to change the response status from within a view -callable that uses a renderer, assign the ``response_status`` attribute to -the request before returning a result: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - - @view_config(name='gone', renderer='templates/gone.pt') - def myview(request): - request.response_status = '404 Not Found' - return {'URL':request.URL} - -For more information on attributes of the request, see the API -documentation in :ref:`request_module`. - .. index:: single: renderer (adding) diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 426ec229b..150b173e3 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -367,13 +367,13 @@ templates as renderers. See :ref:`available_template_system_bindings`. render a view without needing to fork your code to do so. See :ref:`extending_chapter` for more information. -By default, views rendered via a template renderer return a -:term:`Response` object which has a *status code* of ``200 OK``, and a -*content-type* of ``text/html``. To vary attributes of the response -of a view that uses a renderer, such as the content-type, headers, or -status attributes, you must set attributes on the *request* object -within the view before returning the dictionary. See -:ref:`response_request_attrs` for more information. +By default, views rendered via a template renderer return a :term:`Response` +object which has a *status code* of ``200 OK``, and a *content-type* of +``text/html``. To vary attributes of the response of a view that uses a +renderer, such as the content-type, headers, or status attributes, you must +use the API of the :class:`pyramid.response.Response` object exposed as +``request.response`` within the view before returning the dictionary. See +:ref:`request_response_attr` for more information. The same set of system values are provided to templates rendered via a renderer view configuration as those provided to templates rendered diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 219753882..1024dd188 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -54,17 +54,19 @@ Route Configuration ------------------- :term:`Route configuration` is the act of adding a new :term:`route` to an -application. A route has a *pattern*, representing a pattern meant to match +application. A route has a *name*, which acts as an identifier to be used +for URL generation. The name also allows developers to associate a view +configuration with the route. A route also has a *pattern*, meant to match against the ``PATH_INFO`` portion of a URL (the portion following the scheme -and port, e.g. ``/foo/bar`` in the URL ``http://localhost:8080/foo/bar``), -and a *route name*, which is used by developers within a :app:`Pyramid` -application to uniquely identify a particular route when generating a URL. -It also optionally has a ``factory``, a set of :term:`route predicate` -parameters, and a set of :term:`view` parameters. +and port, e.g. ``/foo/bar`` in the URL ``http://localhost:8080/foo/bar``). It +also optionally has a ``factory`` and a set of :term:`route predicate` +attributes. .. index:: single: add_route +.. _config-add-route: + Configuring a Route via The ``add_route`` Configurator Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -79,7 +81,8 @@ example: # pyramid.config.Configurator class; "myview" is assumed # to be a "view callable" function from views import myview - config.add_route('myroute', '/prefix/{one}/{two}', view=myview) + config.add_route('myroute', '/prefix/{one}/{two}') + config.add_view(myview, route_name='myroute') .. versionchanged:: 1.0a4 Prior to 1.0a4, routes allow for a marker starting with a ``:``, for @@ -89,9 +92,47 @@ example: .. index:: single: route configuration; view callable +.. _add_route_view_config: + Route Configuration That Names a View Callable ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. warning:: This section describes a feature which has been deprecated in + Pyramid 1.1 and higher. In order to reduce confusion and documentation + burden, passing view-related parameters to + :meth:`~pyramid.config.Configurator.add_route` is deprecated. + + In versions earlier than 1.1, a view was permitted to be connected to a + route using a set of ``view*`` parameters passed to the + :meth:`~pyramid.config.Configurator.add_route`. This was a shorthand + which replaced the need to perform a subsequent call to + :meth:`~pyramid.config.Configurator.add_view` as described in + :ref:`config-add-route`. For example, it was valid (and often recommended) + to do: + + .. code-block:: python + + config.add_route('home', '/', view='mypackage.views.myview', + view_renderer='some/renderer.pt') + + Instead of the equivalent: + + .. code-block:: python + + config.add_route('home', '/') + config.add_view('mypackage.views.myview', route_name='home') + renderer='some/renderer.pt') + + Passing ``view*`` arguments to ``add_route`` as shown in the first + example above is now deprecated in favor of connecting a view to a + predefined route via :meth:`~pyramid.config.Configurator.add_view` using + the route's ``route_name`` parameter, as shown in the second example + above. + + A deprecation warning is now issued when any view-related parameter is + passed to ``Configurator.add_route``. The recommended way to associate a + view with a route is documented in :ref:`config-add-route`. + When a route configuration declaration names a ``view`` attribute, the value of the attribute will reference a :term:`view callable`. This view callable will be invoked when the route matches. A view callable, as described in @@ -125,6 +166,9 @@ When a route configuration names a ``view`` attribute, the :term:`view callable` named as that ``view`` attribute will always be found and invoked when the associated route pattern matches during a request. +See :meth:`pyramid.config.Configurator.add_route` for a description of +view-related arguments. + .. index:: single: route path pattern syntax @@ -363,8 +407,9 @@ resource of the view callable ultimately found via :term:`view lookup`. .. code-block:: python :linenos: - config.add_route('abc', '/abc', view='myproject.views.theview', + config.add_route('abc', '/abc', factory='myproject.resources.root_factory') + config.add_view('myproject.views.theview', route_name='abc') The factory can either be a Python object or a :term:`dotted Python name` (a string) which points to such a Python object, as it is above. @@ -395,7 +440,8 @@ process. Examples of route predicate arguments are ``pattern``, ``xhr``, and ``request_method``. Other arguments are view configuration related arguments. These only have an -effect when the route configuration names a ``view``. +effect when the route configuration names a ``view``. These arguments have +been deprecated as of :app:`Pyramid` 1.1 (see :ref:`add_route_view_config`). Other arguments are ``name`` and ``factory``. These arguments represent neither predicates nor view configuration information. @@ -547,8 +593,8 @@ If any route matches, the route matching process stops. The :term:`request` is decorated with a special :term:`interface` which describes it as a "route request", the :term:`context` resource is generated, and the context and the resulting request are handed off to :term:`view lookup`. During view lookup, -if any ``view`` argument was provided within the matched route configuration, -the :term:`view callable` it points to is called. +if a :term:`view callable` associated with the matched route is found, that +view is called. When a route configuration is declared, it may contain :term:`route predicate` arguments. All route predicates associated with a route @@ -621,7 +667,8 @@ result in a particular view callable being invoked: .. code-block:: python :linenos: - config.add_route('idea', 'site/{id}', view='mypackage.views.site_view') + config.add_route('idea', 'site/{id}') + config.add_view('mypackage.views.site_view', route_name='idea') When a route configuration with a ``view`` attribute is added to the system, and an incoming request matches the *pattern* of the route configuration, the @@ -665,9 +712,13 @@ add to your application: .. code-block:: python :linenos: - config.add_route('idea', 'ideas/{idea}', view='mypackage.views.idea_view') - config.add_route('user', 'users/{user}', view='mypackage.views.user_view') - config.add_route('tag', 'tags/{tags}', view='mypackage.views.tag_view') + config.add_route('idea', 'ideas/{idea}') + config.add_route('user', 'users/{user}') + config.add_route('tag', 'tags/{tags}') + + config.add_view('mypackage.views.idea_view', route_name='idea') + config.add_view('mypackage.views.user_view', route_name='user') + config.add_view('mypackage.views.tag_view', route_name='tag') The above configuration will allow :app:`Pyramid` to service URLs in these forms: @@ -717,9 +768,8 @@ An example of using a route with a factory: .. code-block:: python :linenos: - config.add_route('idea', 'ideas/{idea}', - view='myproject.views.idea_view', - factory='myproject.resources.Idea') + config.add_route('idea', 'ideas/{idea}', factory='myproject.resources.Idea') + config.add_view('myproject.views.idea_view', route_name='idea') The above route will manufacture an ``Idea`` resource as a :term:`context`, assuming that ``mypackage.resources.Idea`` resolves to a class that accepts a @@ -735,34 +785,6 @@ request in its ``__init__``. For example: In a more complicated application, this root factory might be a class representing a :term:`SQLAlchemy` model. -Example 4 -~~~~~~~~~ - -It is possible to create a route declaration without a ``view`` attribute, -but associate the route with a :term:`view callable` using a ``view`` -declaration. - -.. code-block:: python - :linenos: - - config.add_route('idea', 'site/{id}') - config.add_view(route_name='idea', view='mypackage.views.site_view') - -This set of configuration parameters creates a configuration completely -equivalent to this example provided in :ref:`urldispatch_example1`: - -.. code-block:: python - :linenos: - - config.add_route('idea', 'site/{id}', view='mypackage.views.site_view') - -In fact, the spelling which names a ``view`` attribute is just syntactic -sugar for the more verbose spelling which contains separate view and route -registrations. - -More uses for this style of associating views with routes are explored in -:ref:`hybrid_chapter`. - .. index:: single: matching the root URL single: root url (matching) @@ -777,14 +799,14 @@ It's not entirely obvious how to use a route pattern to match the root URL .. code-block:: python :linenos: - config.add_route('root', '', view='mypackage.views.root_view') + config.add_route('root', '') Or provide the literal string ``/`` as the pattern: .. code-block:: python :linenos: - config.add_route('root', '/', view='mypackage.views.root_view') + config.add_route('root', '/') .. index:: single: generating route URLs @@ -834,10 +856,11 @@ route configuration looks like so: .. code-block:: python :linenos: - config.add_route('noslash', 'no_slash', - view='myproject.views.no_slash') - config.add_route('hasslash', 'has_slash/', - view='myproject.views.has_slash') + config.add_route('noslash', 'no_slash') + config.add_route('hasslash', 'has_slash/') + + config.add_view('myproject.views.no_slash', route_name='noslash') + config.add_view('myproject.views.has_slash', route_name='hasslash') If a request enters the application with the ``PATH_INFO`` value of ``/has_slash/``, the second route will match. If a request enters the @@ -864,8 +887,8 @@ the application's startup configuration, adding the following stanza: .. code-block:: python :linenos: - config.add_view(context='pyramid.exceptions.NotFound', - view='pyramid.view.append_slash_notfound_view') + config.add_view('pyramid.view.append_slash_notfound_view', + context='pyramid.exceptions.NotFound') See :ref:`view_module` and :ref:`changing_the_notfound_view` for more information about the slash-appending not found view and for a more general @@ -1063,30 +1086,25 @@ is executed. Route View Callable Registration and Lookup Details --------------------------------------------------- -The purpose of making it possible to specify a view callable within a route -configuration is to prevent developers from needing to deeply understand the -details of :term:`resource location` and :term:`view lookup`. When a route -names a view callable as a ``view`` argument, and a request enters the system -which matches the pattern of the route, the result is simple: the view -callable associated with the route is invoked with the request that caused -the invocation. +When a request enters the system which matches the pattern of the route, the +usual result is simple: the view callable associated with the route is +invoked with the request that caused the invocation. For most usage, you needn't understand more than this; how it works is an implementation detail. In the interest of completeness, however, we'll explain how it *does* work in the this section. You can skip it if you're uninterested. -When a ``view`` attribute is attached to a route configuration, -:app:`Pyramid` ensures that a :term:`view configuration` is registered that -will always be found when the route pattern is matched during a request. To -do so: +When a view is associated with a route configuration, :app:`Pyramid` ensures +that a :term:`view configuration` is registered that will always be found +when the route pattern is matched during a request. To do so: - A special route-specific :term:`interface` is created at startup time for each route configuration declaration. -- When a route configuration declaration mentions a ``view`` attribute, a +- When an ``add_view`` statement mentions a ``route name`` attribute, a :term:`view configuration` is registered at startup time. This view - configuration uses the route-specific interface as a :term:`request` type. + configuration uses a route-specific interface as a :term:`request` type. - At runtime, when a request causes any route to match, the :term:`request` object is decorated with the route-specific interface. diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 9b2500a2b..743cc016e 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -59,7 +59,8 @@ View configuration is performed in one of these ways: - By specifying a view within a :term:`route configuration`. View configuration via a route configuration is performed by using the :meth:`pyramid.config.Configurator.add_route` method, passing a ``view`` - argument specifying a view callable. + argument specifying a view callable. This pattern of view configuration is + deprecated as of :app:`Pyramid` 1.1. .. note:: A package named ``pyramid_handlers`` (available from PyPI) provides an analogue of :term:`Pylons` -style "controllers", which are a special diff --git a/docs/narr/zca.rst b/docs/narr/zca.rst index fcab0653e..19c52d0c9 100644 --- a/docs/narr/zca.rst +++ b/docs/narr/zca.rst @@ -66,15 +66,15 @@ more than a single application per process. For example, use of a :term:`Paste` "composite" allows you to run separate individual WSGI applications in the same process, each answering requests for some URL prefix. This makes it possible to run, for example, a TurboGears -application at ``/turbogears`` and a BFG application at ``/bfg``, both -served up using the same :term:`WSGI` server within a single Python -process. +application at ``/turbogears`` and a :app:`Pyramid` application at +``/pyramid``, both served up using the same :term:`WSGI` server +within a single Python process. Most production Zope applications are relatively large, making it impractical due to memory constraints to run more than one Zope -application per Python process. However, a :app:`Pyramid` -application may be very small and consume very little memory, so it's -a reasonable goal to be able to run more than one BFG application per +application per Python process. However, a :app:`Pyramid` application +may be very small and consume very little memory, so it's a reasonable +goal to be able to run more than one :app:`Pyramid` application per process. In order to make it possible to run more than one :app:`Pyramid` @@ -182,10 +182,10 @@ global ZCA API. Without special treatment, the ZCA global APIs will always return the global ZCA registry (the one in ``zope.component.globalregistry.base``). -To "fix" this and make the ZCA global APIs use the "current" BFG -registry, you need to call -:meth:`~pyramid.config.Configurator.hook_zca` within your -setup code. For example: +To "fix" this and make the ZCA global APIs use the "current" +:app:`Pyramid` registry, you need to call +:meth:`~pyramid.config.Configurator.hook_zca` within your setup code. +For example: .. code-block:: python :linenos: @@ -253,7 +253,7 @@ Lines 5, 6, and 7 above are the interesting ones. Line 5 retrieves the global ZCA component registry. Line 6 creates a :term:`Configurator`, passing the global ZCA registry into its constructor as the ``registry`` argument. Line 7 "sets up" the global -registry with BFG-specific registrations; this is code that is +registry with Pyramid-specific registrations; this is code that is normally executed when a registry is constructed rather than created, but we must call it "by hand" when we pass an explicit registry. diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index ee86eb543..e4480d6d9 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -7,21 +7,25 @@ edit, and add pages to our wiki. For purposes of demonstration we'll change our application to allow people whom 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. :app:`Pyramid` provides -facilities for *authorization* and *authentication*. We'll make use of both -features to provide security to our application. +facilities for :term:`authorization` and :term:`authentication`. We'll make +use of both features to provide security to our application. -The source code for this tutorial stage can be browsed via -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_. +We will add an :term:`authentication policy` and an +:term:`authorization policy` to our :term:`application +registry`, add a ``security.py`` module and give our :term:`root` +resource an :term:`ACL`. +Then we will add ``login`` and ``logout`` views, and modify the +existing views to make them return a ``logged_in`` flag to the +renderer and add :term:`permission` declarations to their ``view_config`` +decorators. -Configuring a ``pyramid`` Authentication Policy --------------------------------------------------- +Finally, we will add a ``login.pt`` template and change the existing +``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in. -For any :app:`Pyramid` application to perform authorization, we need to add a -``security.py`` module and we'll need to change our :term:`application -registry` to add an :term:`authentication policy` and a :term:`authorization -policy`. +The source code for this tutorial stage can be browsed via +`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/ +<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_. Adding Authentication and Authorization Policies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -64,6 +68,43 @@ user and groups sources. Note that the ``editor`` user is a member of the ``group:editors`` group in our dummy group data (the ``GROUPS`` data structure). +Giving Our Root Resource an ACL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We need to give our root resource object an :term:`ACL`. This ACL will be +sufficient to provide enough information to the :app:`Pyramid` security +machinery to challenge a user who doesn't have appropriate credentials when +he attempts to invoke the ``add_page`` or ``edit_page`` views. + +We need to perform some imports at module scope in our ``models.py`` file: + +.. code-block:: python + :linenos: + + from pyramid.security import Allow + from pyramid.security import Everyone + +Our root resource object is a ``Wiki`` instance. We'll add the following +line at class scope to our ``Wiki`` class: + +.. code-block:: python + :linenos: + + __acl__ = [ (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit') ] + +It's only happenstance that we're assigning this ACL at class scope. An ACL +can be attached to an object *instance* too; this is how "row level security" +can be achieved in :app:`Pyramid` applications. We actually only need *one* +ACL for the entire system, however, because our security requirements are +simple, so this feature is not demonstrated. + +Our resulting ``models.py`` file will now look like so: + +.. literalinclude:: src/authorization/tutorial/models.py + :linenos: + :language: python + Adding Login and Logout Views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -129,6 +170,38 @@ template. For example: logged_in = logged_in, edit_url = edit_url) +Adding ``permission`` Declarations to our ``view_config`` Decorators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To protect each of our views with a particular permission, we need to pass a +``permission`` argument to each of our :class:`pyramid.view.view_config` +decorators. To do so, within ``views.py``: + +- We add ``permission='view'`` to the decorator attached to the + ``view_wiki`` and ``view_page`` view functions. This makes the + assertion that only users who possess the ``view`` permission + against the context resource at the time of the request may + invoke these views. We've granted + :data:`pyramid.security.Everyone` the view permission at the + root model via its ACL, so everyone will be able to invoke the + ``view_wiki`` and ``view_page`` views. + +- We add ``permission='edit'`` to the decorator attached to the + ``add_page`` and ``edit_page`` view functions. This makes the + assertion that only users who possess the effective ``edit`` + permission against the context resource at the time of the + request may invoke these views. We've granted the + ``group:editors`` principal the ``edit`` permission at the + root model via its ACL, so only a user whom is a member of + the group named ``group:editors`` will able to invoke the + ``add_page`` or ``edit_page`` views. We've likewise given + the ``editor`` user membership to this group via the + ``security.py`` file by mapping him to the ``group:editors`` + group in the ``GROUPS`` data structure (``GROUPS + = {'editor':['group:editors']}``); the ``groupfinder`` + function consults the ``GROUPS`` data structure. This means + that the ``editor`` user can add and edit pages. + Adding the ``login.pt`` Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -154,92 +227,29 @@ class="app-welcome align-right">`` div: <a href="${request.application_url}/logout">Logout</a> </span> -Giving Our Root Resource an ACL -------------------------------- - -We need to give our root resource object an :term:`ACL`. This ACL will be -sufficient to provide enough information to the :app:`Pyramid` security -machinery to challenge a user who doesn't have appropriate credentials when -he attempts to invoke the ``add_page`` or ``edit_page`` views. +Seeing Our Changes To ``views.py`` and our Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -We need to perform some imports at module scope in our ``models.py`` file: +Our ``views.py`` module will look something like this when we're done: -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/views.py :linenos: + :language: python - from pyramid.security import Allow - from pyramid.security import Everyone - -Our root resource object is a ``Wiki`` instance. We'll add the following -line at class scope to our ``Wiki`` class: +Our ``edit.pt`` template will look something like this when we're done: -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/templates/edit.pt :linenos: + :language: xml - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] - -It's only happenstance that we're assigning this ACL at class scope. An ACL -can be attached to an object *instance* too; this is how "row level security" -can be achieved in :app:`Pyramid` applications. We actually only need *one* -ACL for the entire system, however, because our security requirements are -simple, so this feature is not demonstrated. - -Our resulting ``models.py`` file will now look like so: +Our ``view.pt`` template will look something like this when we're done: -.. literalinclude:: src/authorization/tutorial/models.py +.. literalinclude:: src/authorization/tutorial/templates/view.pt :linenos: - :language: python - -Adding ``permission`` Declarations to our ``view_config`` Decorators --------------------------------------------------------------------- - -To protect each of our views with a particular permission, we need to pass a -``permission`` argument to each of our :class:`pyramid.view.view_config` -decorators. To do so, within ``views.py``: - -- We add ``permission='view'`` to the decorator attached to the ``view_wiki`` - view function. This makes the assertion that only users who possess the - ``view`` permission against the context resource at the time of the request - may invoke this view. We've granted :data:`pyramid.security.Everyone` the - view permission at the root model via its ACL, so everyone will be able to - invoke the ``view_wiki`` view. - -- We add ``permission='view'`` to the decorator attached to the ``view_page`` - view function. This makes the assertion that only users who possess the - effective ``view`` permission against the context resource at the time of - the request may invoke this view. We've granted - :data:`pyramid.security.Everyone` the view permission at the root model via - its ACL, so everyone will be able to invoke the ``view_page`` view. - -- We add ``permission='edit'`` to the decorator attached to the ``add_page`` - view function. This makes the assertion that only users who possess the - effective ``edit`` permission against the context resource at the time of - the request may invoke this view. We've granted the ``group:editors`` - principal the ``edit`` permission at the root model via its ACL, so only - the a user whom is a member of the group named ``group:editors`` will able - to invoke the ``add_page`` view. We've likewise given the ``editor`` user - membership to this group via thes ``security.py`` file by mapping him to - the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = - {'editor':['group:editors']}``); the ``groupfinder`` function consults the - ``GROUPS`` data structure. This means that the ``editor`` user can add - pages. - -- We add ``permission='edit'`` to the decorator attached to the ``edit_page`` - view function. This makes the assertion that only users who possess the - effective ``edit`` permission against the context resource at the time of - the request may invoke this view. We've granted the ``group:editors`` - principal the ``edit`` permission at the root model via its ACL, so only - the a user whom is a member of the group named ``group:editors`` will able - to invoke the ``edit_page`` view. We've likewise given the ``editor`` user - membership to this group via thes ``security.py`` file by mapping him to - the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = - {'editor':['group:editors']}``); the ``groupfinder`` function consults the - ``GROUPS`` data structure. This means that the ``editor`` user can edit - pages. + :language: xml Viewing the Application in a Browser ------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can finally examine our application in a browser. The views we'll try are as follows: @@ -267,35 +277,7 @@ as follows: credentials with the username ``editor``, password ``editor`` will show the edit page form being displayed. -Seeing Our Changes To ``views.py`` and our Templates ----------------------------------------------------- - -Our ``views.py`` module will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/views.py - :linenos: - :language: python - -Our ``edit.pt`` template will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/templates/edit.pt - :linenos: - :language: xml - -Our ``view.pt`` template will look something like this when we're done: - -.. literalinclude:: src/authorization/tutorial/templates/view.pt - :linenos: - :language: xml - -Revisiting the Application ---------------------------- - -When we revisit the application in a browser, and log in (as a result -of hitting an edit or add page and submitting the login form with the -``editor`` credentials), we'll see a Logout link in the upper right -hand corner. When we click it, we're logged out, and redirected back -to the front page. - - - +- 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/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst index 3d2d01061..baf497458 100644 --- a/docs/tutorials/wiki/definingmodels.rst +++ b/docs/tutorials/wiki/definingmodels.rst @@ -89,70 +89,16 @@ something like this: :linenos: :language: python -Removing View Configuration ---------------------------- - -In a previous step in this chapter, we removed the -``tutorial.models.MyModel`` class. However, our ``views.py`` module still -attempts to import this class. Temporarily, we'll change ``views.py`` so -that it no longer references ``MyModel`` by removing its imports and removing -a reference to it from the arguments passed to the ``@view_config`` -:term:`configuration decoration` decorator which sits atop the ``my_view`` -view callable. - -The result of all of our edits to ``views.py`` will end up looking -something like this: - -.. literalinclude:: src/models/tutorial/views.py - :linenos: - :language: python - -Testing the Models ------------------- - -To make sure the code we just wrote works, we write tests for the model -classes and the appmaker. Changing ``tests.py``, we'll write a separate test -class for each model class, and we'll write a test class for the -``appmaker``. - -To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a -result of the ``pyramid_zodb`` project generator. We'll add three test -classes: one for the ``Page`` model named ``PageModelTests``, one for the -``Wiki`` model named ``WikiModelTests``, and one for the appmaker named -``AppmakerTests``. - -When we're done changing ``tests.py``, it will look something like so: - -.. literalinclude:: src/models/tutorial/tests.py - :linenos: - :language: python +Viewing the Application in a Browser +------------------------------------ -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 output is something like this: +We can't. At this point, our system is in a "non-runnable" state; we'll need +to change view-related files in the next chapter to be able to start the +application successfully. If you try to start the application, you'll wind +up with a Python traceback on your console that ends with this exception: .. code-block:: text - ..... - ---------------------------------------------------------------------- - Ran 5 tests in 0.008s - - OK + ImportError: cannot import name MyModel +This will also happen if you attempt to run the tests. diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 233e571f1..b6c083bbf 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -318,48 +318,3 @@ browser. The views we'll try are as follows: will generate an ``IndexError`` for the expression ``request.subpath[0]``. You'll see an interactive traceback facility provided by :term:`WebError`. - -Testing the Views -================= - -We'll modify our ``tests.py`` file, adding tests for each view function we -added above. As a result, we'll *delete* the ``ViewTests`` test in the file, -and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``, -``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``, -``view_page``, ``add_page``, and ``edit_page`` views respectively. - -Once we're done with the ``tests.py`` module, it will look a lot like the -below: - -.. literalinclude:: src/views/tutorial/tests.py - :linenos: - :language: python - -Running the Tests -================= - -We can run these tests by using ``setup.py test`` in the same way we did in -:ref:`running_tests`. Assuming our shell's current working directory is the -"tutorial" distribution directory: - -On UNIX: - -.. code-block:: 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 9 tests in 0.203s - - OK diff --git a/docs/tutorials/wiki/index.rst b/docs/tutorials/wiki/index.rst index 660bf3bd3..c984c4f01 100644 --- a/docs/tutorials/wiki/index.rst +++ b/docs/tutorials/wiki/index.rst @@ -23,5 +23,6 @@ tutorial can be browsed at definingmodels definingviews authorization + tests distributing diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py new file mode 100644 index 000000000..d9ff866f1 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py @@ -0,0 +1,216 @@ +import unittest + +from pyramid import testing + +class PageModelTests(unittest.TestCase): + + def _getTargetClass(self): + from tutorial.models import Page + return Page + + def _makeOne(self, data=u'some data'): + return self._getTargetClass()(data=data) + + def test_constructor(self): + instance = self._makeOne() + self.assertEqual(instance.data, u'some data') + +class WikiModelTests(unittest.TestCase): + + def _getTargetClass(self): + from tutorial.models import Wiki + return Wiki + + def _makeOne(self): + return self._getTargetClass()() + + def test_it(self): + wiki = self._makeOne() + self.assertEqual(wiki.__parent__, None) + self.assertEqual(wiki.__name__, None) + +class AppmakerTests(unittest.TestCase): + def _callFUT(self, zodb_root): + from tutorial.models import appmaker + return appmaker(zodb_root) + + def test_it(self): + root = {} + self._callFUT(root) + self.assertEqual(root['app_root']['FrontPage'].data, + 'This is the front page') + +class ViewWikiTests(unittest.TestCase): + def test_it(self): + from tutorial.views import view_wiki + context = testing.DummyResource() + request = testing.DummyRequest() + response = view_wiki(context, request) + self.assertEqual(response.location, 'http://example.com/FrontPage') + +class ViewPageTests(unittest.TestCase): + def _callFUT(self, context, request): + from tutorial.views import view_page + return view_page(context, request) + + def test_it(self): + wiki = testing.DummyResource() + wiki['IDoExist'] = testing.DummyResource() + context = testing.DummyResource(data='Hello CruelWorld IDoExist') + context.__parent__ = wiki + context.__name__ = 'thepage' + request = testing.DummyRequest() + info = self._callFUT(context, request) + self.assertEqual(info['page'], context) + 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/thepage/edit_page') + + +class AddPageTests(unittest.TestCase): + def _callFUT(self, context, request): + from tutorial.views import add_page + return add_page(context, request) + + def test_it_notsubmitted(self): + from pyramid.url import resource_url + context = testing.DummyResource() + request = testing.DummyRequest() + request.subpath = ['AnotherPage'] + info = self._callFUT(context, request) + self.assertEqual(info['page'].data,'') + self.assertEqual( + info['save_url'], + resource_url(context, request, 'add_page', 'AnotherPage')) + + def test_it_submitted(self): + context = testing.DummyResource() + request = testing.DummyRequest({'form.submitted':True, + 'body':'Hello yo!'}) + request.subpath = ['AnotherPage'] + self._callFUT(context, request) + page = context['AnotherPage'] + self.assertEqual(page.data, 'Hello yo!') + self.assertEqual(page.__name__, 'AnotherPage') + self.assertEqual(page.__parent__, context) + +class EditPageTests(unittest.TestCase): + def _callFUT(self, context, request): + from tutorial.views import edit_page + return edit_page(context, request) + + def test_it_notsubmitted(self): + from pyramid.url import resource_url + context = testing.DummyResource() + request = testing.DummyRequest() + info = self._callFUT(context, request) + self.assertEqual(info['page'], context) + self.assertEqual(info['save_url'], + resource_url(context, request, 'edit_page')) + + def test_it_submitted(self): + context = testing.DummyResource() + request = testing.DummyRequest({'form.submitted':True, + 'body':'Hello yo!'}) + response = self._callFUT(context, request) + self.assertEqual(response.location, 'http://example.com/') + self.assertEqual(context.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): + import tempfile + import os.path + from tutorial import main + self.tmpdir = tempfile.mkdtemp() + + dbpath = os.path.join( self.tmpdir, 'test.db') + settings = { 'zodb_uri' : 'file://' + dbpath } + + app = main({}, **settings) + from repoze.zodbconn.middleware import EnvironmentDeleterMiddleware + app = EnvironmentDeleterMiddleware(app) + from webtest import TestApp + self.testapp = TestApp(app) + + def tearDown(self): + import shutil + shutil.rmtree( self.tmpdir ) + + 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) + self.assertTrue('Not Found' in res.body) + + 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): + res = 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): + res = self.testapp.get( self.viewer_login, status=302) + res = 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): + res = 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): + res = 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): + res = 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): + res = 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): + res = 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/wiki/tests.rst b/docs/tutorials/wiki/tests.rst new file mode 100644 index 000000000..f3151dbcc --- /dev/null +++ b/docs/tutorials/wiki/tests.rst @@ -0,0 +1,78 @@ +============ +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 tests for the model +classes and the appmaker. Changing ``tests.py``, we'll write a separate test +class for each model class, and we'll write a test class for the +``appmaker``. + +To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a +result of the ``pyramid_zodb`` project generator. We'll add three test +classes: one for the ``Page`` model named ``PageModelTests``, one for the +``Wiki`` model named ``WikiModelTests``, and one for the appmaker named +``AppmakerTests``. + +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 9 tests in 0.203s + + OK diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 64cab30db..19d438fad 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -76,7 +76,14 @@ For any :app:`Pyramid` application to perform authorization, we need to add a We'll change our ``__init__.py`` file to enable an ``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to enable -declarative security checking. +declarative security checking. We need to import the new policies: + +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 2-3,8 + :linenos: + :language: python + +Then, we'll add those policies to the configuration: .. literalinclude:: src/authorization/tutorial/__init__.py :lines: 15-21 @@ -97,25 +104,32 @@ 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: 24-26 + :lines: 24-26,41-43 :linenos: :language: python A forbidden view configures our newly created login view to show up when :app:`Pyramid` detects that a view invocation can not be authorized. -We'll also add ``view_permission`` arguments with the value ``edit`` to the -``edit_page`` and ``add_page`` routes. This indicates that the view -callables which these routes reference cannot be invoked without the +A ``logout`` :term:`view callable` will allow users to log out later: + +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 27-28 + :linenos: + :language: python + +We'll also add ``permission`` arguments with the value ``edit`` to the +``edit_page`` and ``add_page`` views. This indicates that the view +callables which these views reference cannot be invoked without the authenticated user possessing the ``edit`` permission with respect to the current context. .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 32-39 + :lines: 37-40 :linenos: :language: python -Adding these ``view_permission`` arguments causes Pyramid to make the +Adding these ``permission`` arguments causes Pyramid to make the assertion that only users who possess the effective ``edit`` permission at the time of the request may invoke those two views. We've granted the ``group:editors`` principal the ``edit`` permission at the root model via its diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 0dbcf6684..82e112c64 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -81,28 +81,34 @@ via the :meth:`pyramid.config.Configurator.add_route` method that will be used when the URL is ``/``: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13-14 + :lines: 13 :language: py Since this route has a ``pattern`` equalling ``/`` it is the route that will -be called when the URL ``/`` is visted, e.g. ``http://localhost:6543/``. The -argument named ``view`` with the value ``tutorial.views.my_view`` is the +be matched when the URL ``/`` is visted, e.g. ``http://localhost:6543/``. + +Mapping the ``home`` route to code is done by registering a view. You will +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 + :language: py + +The first positional ``add_view`` argument ``tutorial.views.my_view`` is the dotted name to a *function* we write (generated by the ``pyramid_routesalchemy`` scaffold) that is given a ``request`` object and -which returns a response or a dictionary. - -You will use :meth:`pyramid.config.Configurator.add_route` statements in a -:term:`URL dispatch` based application to map URLs to code. This route also -names a ``view_renderer``, which is a template which lives in the -``templates`` subdirectory of the package. When the -``tutorial.views.my_view`` view returns a dictionary, a :term:`renderer` will -use this template to create a response. +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 -Fimnally, we use the :meth:`pyramid.config.Configurator.make_wsgi_app` +Finally, we use the :meth:`pyramid.config.Configurator.make_wsgi_app` method to return a :term:`WSGI` application: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 15 + :lines: 16 :language: py Our final ``__init__.py`` file will look like this: diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 1661753c1..e5d283125 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -26,6 +26,14 @@ The first thing we want to do 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. +Next, we'll remove the :class:`sqlalchemy.Unicode` import and replace it +with :class:`sqlalchemy.Text`. + +.. literalinclude:: src/models/tutorial/models.py + :lines: 5 + :linenos: + :language: py + Then, we'll add a ``Page`` class. Because this is a SQLAlchemy application, this class should inherit from an instance of :class:`sqlalchemy.ext.declarative.declarative_base`. Declarative diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index c5a452d11..832f90b92 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -272,8 +272,8 @@ Mapping Views to URLs in ``__init__.py`` ======================================== The ``__init__.py`` file contains -:meth:`pyramid.config.Configurator.add_route` calls which serve to map -URLs via :term:`url dispatch` to view functions. First, we’ll get rid of the +: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 example and isn’t relevant to our application. @@ -282,21 +282,33 @@ 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 view named ``view_wiki`` in our ``views.py`` file with the name - ``view_wiki``. This is the :term:`default view` for the wiki. + to the route named ``view_wiki``. -#. Add a declaration which maps the pattern ``/{pagename}`` to the view named - ``view_page`` in our ``views.py`` file with the view name ``view_page``. - This is the regular view for a page. +#. Add a declaration which maps the pattern ``/{pagename}`` to the route named + ``view_page``. This is the regular view for a page. -#. Add a declaration which maps the pattern - ``/add_page/{pagename}`` to the view named ``add_page`` in our - ``views.py`` file with the name ``add_page``. This is the add view - for a new page. +#. 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. #. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the - view named ``edit_page`` in our ``views.py`` file with the name - ``edit_page``. This is the edit view for a page. + route named ``edit_page``. This is the edit view for a page. + +After we've defined the routes for our application, we can register views +to handle the processing and rendering that needs to happen when each route is +requested. + +#. Add a declaration which maps the ``view_wiki`` route to the view named + ``view_wiki`` in our ``views.py`` file. This is the :term:`default view` + for the wiki. + +#. Add a declaration which maps the ``view_page`` route to the view named + ``view_page`` in our ``views.py`` file. + +#. Add a declaration which maps the ``add_page`` route to the view named + ``add_page`` in our ``views.py`` file. + +#. Add a declaration which maps the ``edit_page`` route to the view named + ``edit_page`` in our ``views.py`` file. As a result of our edits, the ``__init__.py`` file should look something like so: diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 025b94927..e8baa568c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -20,25 +20,26 @@ def main(global_config, **settings): authentication_policy=authn_policy, authorization_policy=authz_policy) config.add_static_view('static', 'tutorial:static') - config.add_route('view_wiki', '/', view='tutorial.views.view_wiki') - config.add_route('login', '/login', - view='tutorial.login.login', - view_renderer='tutorial:templates/login.pt') - config.add_route('logout', '/logout', - view='tutorial.login.logout') - config.add_route('view_page', '/{pagename}', - view='tutorial.views.view_page', - view_renderer='tutorial:templates/view.pt') - config.add_route('add_page', '/add_page/{pagename}', - view='tutorial.views.add_page', - view_renderer='tutorial:templates/edit.pt', - view_permission='edit') - config.add_route('edit_page', '/{pagename}/edit_page', - view='tutorial.views.edit_page', - view_renderer='tutorial:templates/edit.pt', - view_permission='edit') + + 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.add_route('view_wiki', '/') + + config.add_view('tutorial.login.login', route_name='login', + renderer='tutorial:templates/login.pt') + config.add_view('tutorial.login.logout', route_name='logout') + config.add_view('tutorial.views.view_page', route_name='view_page', + renderer='tutorial:templates/view.pt') + config.add_view('tutorial.views.add_page', route_name='add_page', + renderer='tutorial:templates/edit.pt', permission='edit') + config.add_view('tutorial.views.edit_page', route_name='edit_page', + renderer='tutorial:templates/edit.pt', permission='edit') config.add_view('tutorial.login.login', - renderer='tutorial:templates/login.pt', - context='pyramid.exceptions.Forbidden') + context='pyramid.exceptions.Forbidden', + renderer='tutorial:templates/login.pt') return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py index d27b891c0..c74f07652 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py @@ -10,8 +10,9 @@ def main(global_config, **settings): initialize_sql(engine) config = Configurator(settings=settings) config.add_static_view('static', 'tutorial:static') - config.add_route('home', '/', view='tutorial.views.my_view', - view_renderer='templates/mytemplate.pt') + config.add_route('home', '/') + config.add_view('tutorial.views.my_view', route_name='home', + renderer='templates/mytemplate.pt') return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py index c912a015b..ecc41ca9f 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py @@ -10,6 +10,7 @@ def main(global_config, **settings): initialize_sql(engine) config = Configurator(settings=settings) config.add_static_view('static', 'tutorial:static') - config.add_route('home', '/', view='tutorial.views.my_view', - view_renderer='templates/mytemplate.pt') + config.add_route('home', '/') + config.add_view('tutorial.views.my_view', route_name='home', + renderer='templates/mytemplate.pt') return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index 1a8d24499..ad89c124e 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -10,15 +10,16 @@ def main(global_config, **settings): initialize_sql(engine) config = Configurator(settings=settings) config.add_static_view('static', 'tutorial:static') - config.add_route('view_wiki', '/', view='tutorial.views.view_wiki') - config.add_route('view_page', '/{pagename}', - view='tutorial.views.view_page', - view_renderer='tutorial:templates/view.pt') - config.add_route('add_page', '/add_page/{pagename}', - view='tutorial.views.add_page', - view_renderer='tutorial:templates/edit.pt') - config.add_route('edit_page', '/{pagename}/edit_page', - view='tutorial.views.edit_page', - view_renderer='tutorial:templates/edit.pt') + 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.add_view('tutorial.views.view_wiki', route_name='view_wiki') + config.add_view('tutorial.views.view_page', route_name='view_page', + renderer='tutorial:templates/view.pt') + config.add_view('tutorial.views.add_page', route_name='add_page', + renderer='tutorial:templates/edit.pt') + config.add_view('tutorial.views.edit_page', route_name='edit_page', + renderer='tutorial:templates/edit.pt') return config.make_wsgi_app() |
