From b1b92284f496800a4dfd2cea72cb9be07ba8661c Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Fri, 13 Sep 2013 16:52:14 -0400 Subject: First cut at import of quick tutorial. --- docs/conf.py | 31 ++ docs/index.rst | 4 + docs/quick_tutorial/authentication.rst | 134 ++++++++ docs/quick_tutorial/authentication/development.ini | 42 +++ docs/quick_tutorial/authentication/setup.py | 13 + .../authentication/tutorial/__init__.py | 24 ++ .../quick_tutorial/authentication/tutorial/home.pt | 18 + .../authentication/tutorial/login.pt | 25 ++ .../authentication/tutorial/security.py | 8 + .../authentication/tutorial/tests.py | 47 +++ .../authentication/tutorial/views.py | 64 ++++ docs/quick_tutorial/authorization.rst | 112 +++++++ docs/quick_tutorial/authorization/development.ini | 42 +++ docs/quick_tutorial/authorization/setup.py | 13 + .../authorization/tutorial/__init__.py | 25 ++ docs/quick_tutorial/authorization/tutorial/home.pt | 18 + .../quick_tutorial/authorization/tutorial/login.pt | 25 ++ .../authorization/tutorial/resources.py | 9 + .../authorization/tutorial/security.py | 8 + .../quick_tutorial/authorization/tutorial/tests.py | 47 +++ .../quick_tutorial/authorization/tutorial/views.py | 66 ++++ docs/quick_tutorial/conf.py | 281 ++++++++++++++++ docs/quick_tutorial/databases.rst | 184 ++++++++++ docs/quick_tutorial/databases/development.ini | 49 +++ docs/quick_tutorial/databases/setup.py | 20 ++ docs/quick_tutorial/databases/sqltutorial.sqlite | Bin 0 -> 12288 bytes docs/quick_tutorial/databases/tutorial/__init__.py | 20 ++ .../databases/tutorial/initialize_db.py | 37 ++ docs/quick_tutorial/databases/tutorial/models.py | 35 ++ docs/quick_tutorial/databases/tutorial/tests.py | 58 ++++ docs/quick_tutorial/databases/tutorial/views.py | 96 ++++++ .../quick_tutorial/databases/tutorial/wiki_view.pt | 19 ++ .../databases/tutorial/wikipage_addedit.pt | 22 ++ .../databases/tutorial/wikipage_view.pt | 17 + docs/quick_tutorial/debugtoolbar.rst | 87 +++++ docs/quick_tutorial/debugtoolbar/development.ini | 40 +++ docs/quick_tutorial/debugtoolbar/setup.py | 13 + .../debugtoolbar/tutorial/__init__.py | 13 + docs/quick_tutorial/forms.rst | 146 ++++++++ docs/quick_tutorial/forms/development.ini | 41 +++ docs/quick_tutorial/forms/setup.py | 14 + docs/quick_tutorial/forms/tutorial/__init__.py | 12 + docs/quick_tutorial/forms/tutorial/tests.py | 47 +++ docs/quick_tutorial/forms/tutorial/views.py | 96 ++++++ docs/quick_tutorial/forms/tutorial/wiki_view.pt | 19 ++ .../forms/tutorial/wikipage_addedit.pt | 22 ++ .../quick_tutorial/forms/tutorial/wikipage_view.pt | 17 + docs/quick_tutorial/functional_testing.rst | 68 ++++ .../functional_testing/development.ini | 40 +++ docs/quick_tutorial/functional_testing/setup.py | 13 + .../functional_testing/tutorial/__init__.py | 13 + .../functional_testing/tutorial/tests.py | 31 ++ docs/quick_tutorial/hello_world.rst | 111 ++++++ docs/quick_tutorial/hello_world/app.py | 17 + docs/quick_tutorial/index.rst | 51 +++ docs/quick_tutorial/ini.rst | 144 ++++++++ docs/quick_tutorial/ini/development.ini | 38 +++ docs/quick_tutorial/ini/setup.py | 13 + docs/quick_tutorial/ini/tutorial/__init__.py | 13 + docs/quick_tutorial/jinja2.rst | 96 ++++++ docs/quick_tutorial/jinja2/development.ini | 42 +++ docs/quick_tutorial/jinja2/setup.py | 13 + docs/quick_tutorial/jinja2/tutorial/__init__.py | 9 + docs/quick_tutorial/jinja2/tutorial/home.jinja2 | 9 + docs/quick_tutorial/jinja2/tutorial/home.pt | 9 + docs/quick_tutorial/jinja2/tutorial/tests.py | 50 +++ docs/quick_tutorial/jinja2/tutorial/views.py | 18 + docs/quick_tutorial/json.rst | 101 ++++++ docs/quick_tutorial/json/development.ini | 41 +++ docs/quick_tutorial/json/setup.py | 13 + docs/quick_tutorial/json/tutorial/__init__.py | 10 + docs/quick_tutorial/json/tutorial/home.pt | 9 + docs/quick_tutorial/json/tutorial/tests.py | 50 +++ docs/quick_tutorial/json/tutorial/views.py | 19 ++ docs/quick_tutorial/logging.rst | 77 +++++ docs/quick_tutorial/logging/development.ini | 41 +++ docs/quick_tutorial/logging/setup.py | 13 + docs/quick_tutorial/logging/tutorial/__init__.py | 9 + docs/quick_tutorial/logging/tutorial/home.pt | 9 + docs/quick_tutorial/logging/tutorial/tests.py | 44 +++ docs/quick_tutorial/logging/tutorial/views.py | 23 ++ docs/quick_tutorial/more_view_classes.rst | 180 ++++++++++ .../more_view_classes/development.ini | 41 +++ docs/quick_tutorial/more_view_classes/setup.py | 13 + .../more_view_classes/tutorial/__init__.py | 9 + .../more_view_classes/tutorial/delete.pt | 9 + .../more_view_classes/tutorial/edit.pt | 10 + .../more_view_classes/tutorial/hello.pt | 16 + .../more_view_classes/tutorial/home.pt | 12 + .../more_view_classes/tutorial/tests.py | 31 ++ .../more_view_classes/tutorial/views.py | 39 +++ docs/quick_tutorial/package.rst | 112 +++++++ docs/quick_tutorial/package/setup.py | 9 + docs/quick_tutorial/package/tutorial/__init__.py | 1 + docs/quick_tutorial/package/tutorial/app.py | 17 + docs/quick_tutorial/pyramid_setup.rst | 27 ++ docs/quick_tutorial/python_setup.rst | 88 +++++ docs/quick_tutorial/request_response.rst | 101 ++++++ .../request_response/development.ini | 41 +++ docs/quick_tutorial/request_response/setup.py | 13 + .../request_response/tutorial/__init__.py | 9 + .../request_response/tutorial/home.pt | 9 + .../request_response/tutorial/tests.py | 54 +++ .../request_response/tutorial/views.py | 22 ++ docs/quick_tutorial/rest_ajax.rst | 62 ++++ docs/quick_tutorial/rest_ajax_layout.rst | 50 +++ docs/quick_tutorial/rest_bootstrap.rst | 88 +++++ docs/quick_tutorial/routing.rst | 119 +++++++ docs/quick_tutorial/routing/development.ini | 41 +++ docs/quick_tutorial/routing/setup.py | 13 + docs/quick_tutorial/routing/tutorial/__init__.py | 8 + docs/quick_tutorial/routing/tutorial/home.pt | 10 + docs/quick_tutorial/routing/tutorial/tests.py | 36 ++ docs/quick_tutorial/routing/tutorial/views.py | 20 ++ docs/quick_tutorial/scaffolds.rst | 84 +++++ docs/quick_tutorial/scaffolds/CHANGES.txt | 4 + docs/quick_tutorial/scaffolds/MANIFEST.in | 2 + docs/quick_tutorial/scaffolds/README.txt | 1 + docs/quick_tutorial/scaffolds/development.ini | 60 ++++ docs/quick_tutorial/scaffolds/production.ini | 54 +++ .../quick_tutorial/scaffolds/scaffolds/__init__.py | 11 + .../scaffolds/scaffolds/static/favicon.ico | Bin 0 -> 1406 bytes .../scaffolds/scaffolds/static/footerbg.png | Bin 0 -> 333 bytes .../scaffolds/scaffolds/static/headerbg.png | Bin 0 -> 203 bytes .../scaffolds/scaffolds/static/ie6.css | 8 + .../scaffolds/scaffolds/static/middlebg.png | Bin 0 -> 2797 bytes .../scaffolds/scaffolds/static/pylons.css | 372 +++++++++++++++++++++ .../scaffolds/scaffolds/static/pyramid-small.png | Bin 0 -> 7044 bytes .../scaffolds/scaffolds/static/pyramid.png | Bin 0 -> 33055 bytes .../scaffolds/scaffolds/static/transparent.gif | Bin 0 -> 49 bytes .../scaffolds/scaffolds/templates/mytemplate.pt | 76 +++++ docs/quick_tutorial/scaffolds/scaffolds/tests.py | 17 + docs/quick_tutorial/scaffolds/scaffolds/views.py | 6 + docs/quick_tutorial/scaffolds/setup.cfg | 27 ++ docs/quick_tutorial/scaffolds/setup.py | 39 +++ docs/quick_tutorial/sessions.rst | 98 ++++++ docs/quick_tutorial/sessions/development.ini | 41 +++ docs/quick_tutorial/sessions/setup.py | 13 + docs/quick_tutorial/sessions/tutorial/__init__.py | 13 + docs/quick_tutorial/sessions/tutorial/home.pt | 10 + docs/quick_tutorial/sessions/tutorial/tests.py | 44 +++ docs/quick_tutorial/sessions/tutorial/views.py | 29 ++ docs/quick_tutorial/static_assets.rst | 89 +++++ docs/quick_tutorial/static_assets/development.ini | 41 +++ docs/quick_tutorial/static_assets/setup.py | 13 + .../static_assets/tutorial/__init__.py | 10 + docs/quick_tutorial/static_assets/tutorial/home.pt | 11 + .../static_assets/tutorial/static/app.css | 4 + .../quick_tutorial/static_assets/tutorial/tests.py | 44 +++ .../quick_tutorial/static_assets/tutorial/views.py | 18 + docs/quick_tutorial/templating.rst | 100 ++++++ docs/quick_tutorial/templating/development.ini | 41 +++ docs/quick_tutorial/templating/setup.py | 13 + .../quick_tutorial/templating/tutorial/__init__.py | 9 + docs/quick_tutorial/templating/tutorial/home.pt | 9 + docs/quick_tutorial/templating/tutorial/tests.py | 44 +++ docs/quick_tutorial/templating/tutorial/views.py | 13 + docs/quick_tutorial/traversal_addcontent.rst | 105 ++++++ docs/quick_tutorial/traversal_hierarchy.rst | 106 ++++++ docs/quick_tutorial/traversal_siteroot.rst | 153 +++++++++ docs/quick_tutorial/traversal_typeviews.rst | 112 +++++++ docs/quick_tutorial/traversal_zodb.rst | 121 +++++++ docs/quick_tutorial/tutorial_approach.rst | 45 +++ docs/quick_tutorial/unit_testing.rst | 117 +++++++ docs/quick_tutorial/unit_testing/development.ini | 40 +++ docs/quick_tutorial/unit_testing/setup.py | 13 + .../unit_testing/tutorial/__init__.py | 13 + docs/quick_tutorial/unit_testing/tutorial/tests.py | 18 + docs/quick_tutorial/view_classes.rst | 96 ++++++ docs/quick_tutorial/view_classes/development.ini | 41 +++ docs/quick_tutorial/view_classes/setup.py | 13 + .../view_classes/tutorial/__init__.py | 9 + docs/quick_tutorial/view_classes/tutorial/home.pt | 9 + docs/quick_tutorial/view_classes/tutorial/tests.py | 44 +++ docs/quick_tutorial/view_classes/tutorial/views.py | 17 + docs/quick_tutorial/views.rst | 120 +++++++ docs/quick_tutorial/views/development.ini | 40 +++ docs/quick_tutorial/views/setup.py | 13 + docs/quick_tutorial/views/tutorial/__init__.py | 9 + docs/quick_tutorial/views/tutorial/tests.py | 44 +++ docs/quick_tutorial/views/tutorial/views.py | 14 + 181 files changed, 7527 insertions(+) create mode 100644 docs/quick_tutorial/authentication.rst create mode 100644 docs/quick_tutorial/authentication/development.ini create mode 100644 docs/quick_tutorial/authentication/setup.py create mode 100644 docs/quick_tutorial/authentication/tutorial/__init__.py create mode 100644 docs/quick_tutorial/authentication/tutorial/home.pt create mode 100644 docs/quick_tutorial/authentication/tutorial/login.pt create mode 100644 docs/quick_tutorial/authentication/tutorial/security.py create mode 100644 docs/quick_tutorial/authentication/tutorial/tests.py create mode 100644 docs/quick_tutorial/authentication/tutorial/views.py create mode 100644 docs/quick_tutorial/authorization.rst create mode 100644 docs/quick_tutorial/authorization/development.ini create mode 100644 docs/quick_tutorial/authorization/setup.py create mode 100644 docs/quick_tutorial/authorization/tutorial/__init__.py create mode 100644 docs/quick_tutorial/authorization/tutorial/home.pt create mode 100644 docs/quick_tutorial/authorization/tutorial/login.pt create mode 100644 docs/quick_tutorial/authorization/tutorial/resources.py create mode 100644 docs/quick_tutorial/authorization/tutorial/security.py create mode 100644 docs/quick_tutorial/authorization/tutorial/tests.py create mode 100644 docs/quick_tutorial/authorization/tutorial/views.py create mode 100644 docs/quick_tutorial/conf.py create mode 100644 docs/quick_tutorial/databases.rst create mode 100644 docs/quick_tutorial/databases/development.ini create mode 100644 docs/quick_tutorial/databases/setup.py create mode 100644 docs/quick_tutorial/databases/sqltutorial.sqlite create mode 100644 docs/quick_tutorial/databases/tutorial/__init__.py create mode 100644 docs/quick_tutorial/databases/tutorial/initialize_db.py create mode 100644 docs/quick_tutorial/databases/tutorial/models.py create mode 100644 docs/quick_tutorial/databases/tutorial/tests.py create mode 100644 docs/quick_tutorial/databases/tutorial/views.py create mode 100644 docs/quick_tutorial/databases/tutorial/wiki_view.pt create mode 100644 docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt create mode 100644 docs/quick_tutorial/databases/tutorial/wikipage_view.pt create mode 100644 docs/quick_tutorial/debugtoolbar.rst create mode 100644 docs/quick_tutorial/debugtoolbar/development.ini create mode 100644 docs/quick_tutorial/debugtoolbar/setup.py create mode 100644 docs/quick_tutorial/debugtoolbar/tutorial/__init__.py create mode 100644 docs/quick_tutorial/forms.rst create mode 100644 docs/quick_tutorial/forms/development.ini create mode 100644 docs/quick_tutorial/forms/setup.py create mode 100644 docs/quick_tutorial/forms/tutorial/__init__.py create mode 100644 docs/quick_tutorial/forms/tutorial/tests.py create mode 100644 docs/quick_tutorial/forms/tutorial/views.py create mode 100644 docs/quick_tutorial/forms/tutorial/wiki_view.pt create mode 100644 docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt create mode 100644 docs/quick_tutorial/forms/tutorial/wikipage_view.pt create mode 100644 docs/quick_tutorial/functional_testing.rst create mode 100644 docs/quick_tutorial/functional_testing/development.ini create mode 100644 docs/quick_tutorial/functional_testing/setup.py create mode 100644 docs/quick_tutorial/functional_testing/tutorial/__init__.py create mode 100644 docs/quick_tutorial/functional_testing/tutorial/tests.py create mode 100644 docs/quick_tutorial/hello_world.rst create mode 100644 docs/quick_tutorial/hello_world/app.py create mode 100644 docs/quick_tutorial/index.rst create mode 100644 docs/quick_tutorial/ini.rst create mode 100644 docs/quick_tutorial/ini/development.ini create mode 100644 docs/quick_tutorial/ini/setup.py create mode 100644 docs/quick_tutorial/ini/tutorial/__init__.py create mode 100644 docs/quick_tutorial/jinja2.rst create mode 100644 docs/quick_tutorial/jinja2/development.ini create mode 100644 docs/quick_tutorial/jinja2/setup.py create mode 100644 docs/quick_tutorial/jinja2/tutorial/__init__.py create mode 100644 docs/quick_tutorial/jinja2/tutorial/home.jinja2 create mode 100644 docs/quick_tutorial/jinja2/tutorial/home.pt create mode 100644 docs/quick_tutorial/jinja2/tutorial/tests.py create mode 100644 docs/quick_tutorial/jinja2/tutorial/views.py create mode 100644 docs/quick_tutorial/json.rst create mode 100644 docs/quick_tutorial/json/development.ini create mode 100644 docs/quick_tutorial/json/setup.py create mode 100644 docs/quick_tutorial/json/tutorial/__init__.py create mode 100644 docs/quick_tutorial/json/tutorial/home.pt create mode 100644 docs/quick_tutorial/json/tutorial/tests.py create mode 100644 docs/quick_tutorial/json/tutorial/views.py create mode 100644 docs/quick_tutorial/logging.rst create mode 100644 docs/quick_tutorial/logging/development.ini create mode 100644 docs/quick_tutorial/logging/setup.py create mode 100644 docs/quick_tutorial/logging/tutorial/__init__.py create mode 100644 docs/quick_tutorial/logging/tutorial/home.pt create mode 100644 docs/quick_tutorial/logging/tutorial/tests.py create mode 100644 docs/quick_tutorial/logging/tutorial/views.py create mode 100644 docs/quick_tutorial/more_view_classes.rst create mode 100644 docs/quick_tutorial/more_view_classes/development.ini create mode 100644 docs/quick_tutorial/more_view_classes/setup.py create mode 100644 docs/quick_tutorial/more_view_classes/tutorial/__init__.py create mode 100644 docs/quick_tutorial/more_view_classes/tutorial/delete.pt create mode 100644 docs/quick_tutorial/more_view_classes/tutorial/edit.pt create mode 100644 docs/quick_tutorial/more_view_classes/tutorial/hello.pt create mode 100644 docs/quick_tutorial/more_view_classes/tutorial/home.pt create mode 100644 docs/quick_tutorial/more_view_classes/tutorial/tests.py create mode 100644 docs/quick_tutorial/more_view_classes/tutorial/views.py create mode 100644 docs/quick_tutorial/package.rst create mode 100644 docs/quick_tutorial/package/setup.py create mode 100644 docs/quick_tutorial/package/tutorial/__init__.py create mode 100644 docs/quick_tutorial/package/tutorial/app.py create mode 100644 docs/quick_tutorial/pyramid_setup.rst create mode 100644 docs/quick_tutorial/python_setup.rst create mode 100644 docs/quick_tutorial/request_response.rst create mode 100644 docs/quick_tutorial/request_response/development.ini create mode 100644 docs/quick_tutorial/request_response/setup.py create mode 100644 docs/quick_tutorial/request_response/tutorial/__init__.py create mode 100644 docs/quick_tutorial/request_response/tutorial/home.pt create mode 100644 docs/quick_tutorial/request_response/tutorial/tests.py create mode 100644 docs/quick_tutorial/request_response/tutorial/views.py create mode 100644 docs/quick_tutorial/rest_ajax.rst create mode 100644 docs/quick_tutorial/rest_ajax_layout.rst create mode 100644 docs/quick_tutorial/rest_bootstrap.rst create mode 100644 docs/quick_tutorial/routing.rst create mode 100644 docs/quick_tutorial/routing/development.ini create mode 100644 docs/quick_tutorial/routing/setup.py create mode 100644 docs/quick_tutorial/routing/tutorial/__init__.py create mode 100644 docs/quick_tutorial/routing/tutorial/home.pt create mode 100644 docs/quick_tutorial/routing/tutorial/tests.py create mode 100644 docs/quick_tutorial/routing/tutorial/views.py create mode 100644 docs/quick_tutorial/scaffolds.rst create mode 100644 docs/quick_tutorial/scaffolds/CHANGES.txt create mode 100644 docs/quick_tutorial/scaffolds/MANIFEST.in create mode 100644 docs/quick_tutorial/scaffolds/README.txt create mode 100644 docs/quick_tutorial/scaffolds/development.ini create mode 100644 docs/quick_tutorial/scaffolds/production.ini create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/__init__.py create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/favicon.ico create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/footerbg.png create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/headerbg.png create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/ie6.css create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/middlebg.png create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/pylons.css create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/pyramid-small.png create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/pyramid.png create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/static/transparent.gif create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/templates/mytemplate.pt create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/tests.py create mode 100644 docs/quick_tutorial/scaffolds/scaffolds/views.py create mode 100644 docs/quick_tutorial/scaffolds/setup.cfg create mode 100644 docs/quick_tutorial/scaffolds/setup.py create mode 100644 docs/quick_tutorial/sessions.rst create mode 100644 docs/quick_tutorial/sessions/development.ini create mode 100644 docs/quick_tutorial/sessions/setup.py create mode 100644 docs/quick_tutorial/sessions/tutorial/__init__.py create mode 100644 docs/quick_tutorial/sessions/tutorial/home.pt create mode 100644 docs/quick_tutorial/sessions/tutorial/tests.py create mode 100644 docs/quick_tutorial/sessions/tutorial/views.py create mode 100644 docs/quick_tutorial/static_assets.rst create mode 100644 docs/quick_tutorial/static_assets/development.ini create mode 100644 docs/quick_tutorial/static_assets/setup.py create mode 100644 docs/quick_tutorial/static_assets/tutorial/__init__.py create mode 100644 docs/quick_tutorial/static_assets/tutorial/home.pt create mode 100644 docs/quick_tutorial/static_assets/tutorial/static/app.css create mode 100644 docs/quick_tutorial/static_assets/tutorial/tests.py create mode 100644 docs/quick_tutorial/static_assets/tutorial/views.py create mode 100644 docs/quick_tutorial/templating.rst create mode 100644 docs/quick_tutorial/templating/development.ini create mode 100644 docs/quick_tutorial/templating/setup.py create mode 100644 docs/quick_tutorial/templating/tutorial/__init__.py create mode 100644 docs/quick_tutorial/templating/tutorial/home.pt create mode 100644 docs/quick_tutorial/templating/tutorial/tests.py create mode 100644 docs/quick_tutorial/templating/tutorial/views.py create mode 100644 docs/quick_tutorial/traversal_addcontent.rst create mode 100644 docs/quick_tutorial/traversal_hierarchy.rst create mode 100644 docs/quick_tutorial/traversal_siteroot.rst create mode 100644 docs/quick_tutorial/traversal_typeviews.rst create mode 100644 docs/quick_tutorial/traversal_zodb.rst create mode 100644 docs/quick_tutorial/tutorial_approach.rst create mode 100644 docs/quick_tutorial/unit_testing.rst create mode 100644 docs/quick_tutorial/unit_testing/development.ini create mode 100644 docs/quick_tutorial/unit_testing/setup.py create mode 100644 docs/quick_tutorial/unit_testing/tutorial/__init__.py create mode 100644 docs/quick_tutorial/unit_testing/tutorial/tests.py create mode 100644 docs/quick_tutorial/view_classes.rst create mode 100644 docs/quick_tutorial/view_classes/development.ini create mode 100644 docs/quick_tutorial/view_classes/setup.py create mode 100644 docs/quick_tutorial/view_classes/tutorial/__init__.py create mode 100644 docs/quick_tutorial/view_classes/tutorial/home.pt create mode 100644 docs/quick_tutorial/view_classes/tutorial/tests.py create mode 100644 docs/quick_tutorial/view_classes/tutorial/views.py create mode 100644 docs/quick_tutorial/views.rst create mode 100644 docs/quick_tutorial/views/development.ini create mode 100644 docs/quick_tutorial/views/setup.py create mode 100644 docs/quick_tutorial/views/tutorial/__init__.py create mode 100644 docs/quick_tutorial/views/tutorial/tests.py create mode 100644 docs/quick_tutorial/views/tutorial/views.py (limited to 'docs') diff --git a/docs/conf.py b/docs/conf.py index a7a4a441a..799ec7820 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -90,8 +90,39 @@ intersphinx_mapping = { 'zcml': ('http://docs.pylonsproject.org/projects/pyramid_zcml/en/latest', None), + 'pyramid': ( + 'http://docs.pylonsproject.org/projects/pyramid/en/latest/', + None) } + +#intersphinx_mapping = { +# 'python': ( +# 'http://docs.python.org/2', +# None), +# 'sqla': ( +# 'http://docs.sqlalchemy.org/en/latest', +# None), +# 'pyramid': ( +# 'http://docs.pylonsproject.org/projects/pyramid/en/latest/', +# None), +# 'jinja2': ( +# 'http://docs.pylonsproject.org/projects/pyramid_jinja2/en/latest/', +# None), +# 'toolbar': ( +# 'http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest', +# None), +# 'deform': ( +# 'http://docs.pylonsproject.org/projects/deform/en/latest', +# None), +# 'colander': ( +# 'http://docs.pylonsproject.org/projects/colander/en/latest', +# None), +# 'tutorials': ( +# 'http://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/', +# None), +#} + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/index.rst b/docs/index.rst index 2efe90cf7..78a00966d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,10 +44,14 @@ up to speed right away. :hidden: quick_tour + quick_tutorial/index * :doc:`quick_tour` goes through the major features in Pyramid, covering a little about a lot. +* :doc:`quick_tutorial/index` does the same, but in a tutorial format: + deeper treatment of each topic and with working code. + * To see a minimal Pyramid web application, check out :ref:`firstapp_chapter`. diff --git a/docs/quick_tutorial/authentication.rst b/docs/quick_tutorial/authentication.rst new file mode 100644 index 000000000..6b9b5fafc --- /dev/null +++ b/docs/quick_tutorial/authentication.rst @@ -0,0 +1,134 @@ +============================== +20: Logins With Authentication +============================== + +Login views that authenticate a username/password against a list of +users. + +Background +========== + +Most web applications have URLs that allow people to add/edit/delete +content via a web browser. Time to add +:ref:`security ` +to the application. In this first step we introduce authentication. +That is, logging in and logging out using Pyramid's rich facilities for +pluggable user storages. + +In the next step we will introduce protection resources with +authorization security statements. + +Objectives +========== + +- Introduce the Pyramid concepts of authentication + +- Create login/logout views + +Steps +===== + +#. We are going to use the view classes step as our starting point: + + .. code-block:: bash + + (env27)$ cd ..; cp -r view_classes authentication; cd authentication + (env27)$ python setup.py develop + +#. Put the security hash in the ``authentication/development.ini`` + configuration file as ``tutorial.secret`` instead of putting it in + the code: + + .. literalinclude:: authentication/development.ini + :language: ini + :linenos: + +#. Get authentication (and for now, authorization policies) and login + route into the :term:`configurator` in + ``authentication/tutorial/__init__.py``: + + .. literalinclude:: authentication/tutorial/__init__.py + :linenos: + +#. Create a ``authentication/tutorial/security.py`` module that can find + our user information by providing an *authentication policy callback*: + + .. literalinclude:: authentication/tutorial/security.py + :linenos: + +#. Update the views in ``authentication/tutorial/views.py``: + + .. literalinclude:: authentication/tutorial/views.py + :linenos: + +#. Add a login template at ``authentication/tutorial/login.pt``: + + .. literalinclude:: authentication/tutorial/login.pt + :language: html + :linenos: + +#. Provide a login/logout box in ``authentication/tutorial/home.pt`` + + .. literalinclude:: authentication/tutorial/home.pt + :language: html + :linenos: + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/`` in a browser. + +#. Click the "Log In" link. + +#. Submit the login form with the username ``editor`` and the password + ``editor``. + +#. Note that the "Log In" link has changed to "Logout". + +#. Click the "Logout" link. + +Analysis +======== + +Unlike many web frameworks, Pyramid includes a built-in (but optional) +security model for authentication and authorization. This security +system is intended to be flexible and support many needs. In this +security model, authentication (who are you) and authorization (what +are you allowed to do) are not just pluggable, but de-coupled. To learn +one step at a time, we provide a system that identifies users and lets +them log out. + +In this example we chose to use the bundled +:ref:`AuthTktAuthenticationPolicy ` +policy. We enabled it in our configuration and provided a +ticket-signing secret in our INI file. + +Our view class grew a login view. When you reached it via a GET, +it returned a login form. When reached via POST, it processed the +username and password against the "groupfinder" callable that we +registered in the configuration. + +In our template, we fetched the ``logged_in`` value from the view +class. We use this to calculate the logged-in user, +if any. In the template we can then choose to show a login link to +anonymous visitors or a logout link to logged-in users. + +Extra Credit +============ + +#. What is the difference between a user and a principal? + +#. Can I use a database behind my ``groupfinder`` to look up principals? + +#. Do I have to put a ``renderer`` in my ``@forbidden_view_config`` + decorator? + +#. Once I am logged in, does any user-centric information get jammed + onto each request? Use ``import pdb; pdb.set_trace()`` to answer + this. + +.. seealso:: :ref:`pyramid:security_chapter`, + :ref:`AuthTktAuthenticationPolicy ` diff --git a/docs/quick_tutorial/authentication/development.ini b/docs/quick_tutorial/authentication/development.ini new file mode 100644 index 000000000..5d4580ff5 --- /dev/null +++ b/docs/quick_tutorial/authentication/development.ini @@ -0,0 +1,42 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar +tutorial.secret = 98zd + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/authentication/setup.py b/docs/quick_tutorial/authentication/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/authentication/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/__init__.py b/docs/quick_tutorial/authentication/tutorial/__init__.py new file mode 100644 index 000000000..7aa049427 --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/__init__.py @@ -0,0 +1,24 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.config import Configurator + +from .security import groupfinder + + +def main(global_config, **settings): + config = Configurator(settings=settings) + + # Security policies + authn_policy = AuthTktAuthenticationPolicy( + settings['tutorial.secret'], callback=groupfinder, + hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.scan('.views') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/home.pt b/docs/quick_tutorial/authentication/tutorial/home.pt new file mode 100644 index 000000000..6ecd0081b --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/home.pt @@ -0,0 +1,18 @@ + + + + Quick Tour: ${name} + + + +
+ Log In + Logout +
+ +

Hi ${name}

+

Visit hello

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/login.pt b/docs/quick_tutorial/authentication/tutorial/login.pt new file mode 100644 index 000000000..4451fc4f8 --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/login.pt @@ -0,0 +1,25 @@ + + + + Quick Tour: ${name} + + +

Login

+ + +
+ + +
+ +
+ +
+ + \ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/security.py b/docs/quick_tutorial/authentication/tutorial/security.py new file mode 100644 index 000000000..ab90bab2c --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/security.py @@ -0,0 +1,8 @@ +USERS = {'editor': 'editor', + 'viewer': 'viewer'} +GROUPS = {'editor': ['group:editors']} + + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, []) \ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/tests.py b/docs/quick_tutorial/authentication/tutorial/tests.py new file mode 100644 index 000000000..6ff554a1e --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/tests.py @@ -0,0 +1,47 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def tearDown(self): + testing.tearDown() + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'

Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'

Hi Hello View', res.body) diff --git a/docs/quick_tutorial/authentication/tutorial/views.py b/docs/quick_tutorial/authentication/tutorial/views.py new file mode 100644 index 000000000..3038b6d9b --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/views.py @@ -0,0 +1,64 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + authenticated_userid + ) +from pyramid.view import ( + view_config, + view_defaults + ) + +from .security import USERS + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + self.logged_in = authenticated_userid(request) + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} + + @view_config(route_name='login', renderer='login.pt') + def login(self): + request = self.request + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) + message = 'Failed login' + + return dict( + name='Login', + message=message, + url=request.application_url + '/login', + came_from=came_from, + login=login, + password=password, + ) + + @view_config(route_name='logout') + def logout(self): + request = self.request + headers = forget(request) + url = request.route_url('home') + return HTTPFound(location=url, + headers=headers) diff --git a/docs/quick_tutorial/authorization.rst b/docs/quick_tutorial/authorization.rst new file mode 100644 index 000000000..37b1a0520 --- /dev/null +++ b/docs/quick_tutorial/authorization.rst @@ -0,0 +1,112 @@ +=========================================== +21: Protecting Resources With Authorization +=========================================== + +Assign security statements to resources describing the permissions +required to perform an operation. + +Background +========== + +Our application has URLs that allow people to add/edit/delete content +via a web browser. Time to add security to the application. Let's +protect our add/edit views to require a login (username of +``editor`` and password of ``editor``.) We will allow the other views +to continue working without a password. + +Objectives +========== + +- Introduce the Pyramid concepts of authentication, authorization, + permissions, and access control lists (ACLs) + +- Make a :term:`root factory` that returns an instance of our + class for the top of the application + +- Assign security statements to our root resource + +- Add a permissions predicate on a view + +- Provide a :term:`Forbidden view` to handle visiting a URL without + adequate permissions + +Steps +===== + +#. We are going to use the authentication step as our starting point: + + .. code-block:: bash + + (env27)$ cd ..; cp -r authentication authorization; cd authorization + (env27)$ python setup.py develop + +#. Start by changing ``authorization/tutorial/__init__.py`` to + specify a root factory to the :term:`pyramid:configurator`: + + .. literalinclude:: authorization/tutorial/__init__.py + :linenos: + +#. That means we need to implement + ``authorization/tutorial/resources.py`` + + .. literalinclude:: authorization/tutorial/resources.py + :linenos: + +#. Change ``authorization/tutorial/views.py`` to require the ``edit`` + permission on the ``hello`` view and implement the forbidden view: + + .. literalinclude:: authorization/tutorial/views.py + :linenos: + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/`` in a browser. + +#. If you are still logged in, click the "Log Out" link. + +#. Visit ``http://localhost:6543/howdy`` in a browser. You should be + asked to login. + +Analysis +======== + +This simple tutorial step can be boiled down to the following: + +- A view can require a *permission* (``edit``) + +- The context for our view (the ``Root``) has an access control list + (ACL) + +- This ACL says that the ``edit`` permission is available on ``Root`` + to the ``group:editors`` *principal* + +- The registered ``groupfinder`` answers whether a particular user + (``editor``) has a particular group (``group:editors``) + +In summary: ``hello`` wants ``edit`` permission, ``Root`` says +``group:editors`` has ``edit`` permission. + +Of course, this only applies on ``Root``. Some other part of the site +(a.k.a. *context*) might have a different ACL. + +If you are not logged in and visit ``/hello``, you need to get +shown the login screen. How does Pyramid know what is the login page to +use? We explicitly told Pyramid that the ``login`` view should be used +by decorating the view with ``@forbidden_view_config``. + +Extra Credit +============ + +#. Perhaps you would like experience of not having enough permissions + (forbidden) to be richer. How could you change this? + +#. Perhaps we want to store security statements in a database and + allow editing via a browser. How might this be done? + +#. What if we want different security statements on different kinds of + objects? Or on the same kinds of objects, but in different parts of a + URL hierarchy? diff --git a/docs/quick_tutorial/authorization/development.ini b/docs/quick_tutorial/authorization/development.ini new file mode 100644 index 000000000..5d4580ff5 --- /dev/null +++ b/docs/quick_tutorial/authorization/development.ini @@ -0,0 +1,42 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar +tutorial.secret = 98zd + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/authorization/setup.py b/docs/quick_tutorial/authorization/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/authorization/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/__init__.py b/docs/quick_tutorial/authorization/tutorial/__init__.py new file mode 100644 index 000000000..715a14203 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/__init__.py @@ -0,0 +1,25 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.config import Configurator + +from .security import groupfinder + + +def main(global_config, **settings): + config = Configurator(settings=settings, + root_factory='.resources.Root') + + # Security policies + authn_policy = AuthTktAuthenticationPolicy( + settings['tutorial.secret'], callback=groupfinder, + hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.scan('.views') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/home.pt b/docs/quick_tutorial/authorization/tutorial/home.pt new file mode 100644 index 000000000..6ecd0081b --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/home.pt @@ -0,0 +1,18 @@ + + + + Quick Tour: ${name} + + + +
+ Log In + Logout +
+ +

Hi ${name}

+

Visit hello

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/login.pt b/docs/quick_tutorial/authorization/tutorial/login.pt new file mode 100644 index 000000000..4451fc4f8 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/login.pt @@ -0,0 +1,25 @@ + + + + Quick Tour: ${name} + + +

Login

+ + +
+ + +
+ +
+ +
+ + \ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/resources.py b/docs/quick_tutorial/authorization/tutorial/resources.py new file mode 100644 index 000000000..0cb656f12 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/resources.py @@ -0,0 +1,9 @@ +from pyramid.security import Allow, Everyone + + +class Root(object): + __acl__ = [(Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit')] + + def __init__(self, request): + pass \ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/security.py b/docs/quick_tutorial/authorization/tutorial/security.py new file mode 100644 index 000000000..ab90bab2c --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/security.py @@ -0,0 +1,8 @@ +USERS = {'editor': 'editor', + 'viewer': 'viewer'} +GROUPS = {'editor': ['group:editors']} + + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, []) \ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/tests.py b/docs/quick_tutorial/authorization/tutorial/tests.py new file mode 100644 index 000000000..6ff554a1e --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/tests.py @@ -0,0 +1,47 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def tearDown(self): + testing.tearDown() + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'

Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'

Hi Hello View', res.body) diff --git a/docs/quick_tutorial/authorization/tutorial/views.py b/docs/quick_tutorial/authorization/tutorial/views.py new file mode 100644 index 000000000..92c1946ba --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/views.py @@ -0,0 +1,66 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + authenticated_userid + ) +from pyramid.view import ( + view_config, + view_defaults, + forbidden_view_config + ) + +from .security import USERS + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + self.logged_in = authenticated_userid(request) + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello', permission='edit') + def hello(self): + return {'name': 'Hello View'} + + @view_config(route_name='login', renderer='login.pt') + @forbidden_view_config(renderer='login.pt') + def login(self): + request = self.request + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) + message = 'Failed login' + + return dict( + name='Login', + message=message, + url=request.application_url + '/login', + came_from=came_from, + login=login, + password=password, + ) + + @view_config(route_name='logout') + def logout(self): + request = self.request + headers = forget(request) + url = request.route_url('home') + return HTTPFound(location=url, + headers=headers) diff --git a/docs/quick_tutorial/conf.py b/docs/quick_tutorial/conf.py new file mode 100644 index 000000000..47b8fae41 --- /dev/null +++ b/docs/quick_tutorial/conf.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +# +# Getting Started with Pyramid and REST documentation build configuration file, created by +# sphinx-quickstart on Mon Aug 26 14:44:57 2013. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.intersphinx'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Getting Started with Pyramid and REST' +copyright = u'2013, Agendaless Consulting' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'GettingStartedwithPyramidandRESTdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + #'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'GettingStartedwithPyramidandREST.tex', + u'Getting Started with Pyramid and REST Documentation', + u'Agendaless Consulting', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'gettingstartedwithpyramidandrest', + u'Getting Started with Pyramid and REST Documentation', + [u'Agendaless Consulting'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'GettingStartedwithPyramidandREST', + u'Getting Started with Pyramid and REST Documentation', + u'Agendaless Consulting', 'GettingStartedwithPyramidandREST', + 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ( + 'http://docs.python.org/2', + None), + 'sqla': ( + 'http://docs.sqlalchemy.org/en/latest', + None), + 'pyramid': ( + 'http://docs.pylonsproject.org/projects/pyramid/en/latest/', + None), + 'jinja2': ( + 'http://docs.pylonsproject.org/projects/pyramid_jinja2/en/latest/', + None), + 'toolbar': ( + 'http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest', + None), + 'deform': ( + 'http://docs.pylonsproject.org/projects/deform/en/latest', + None), + 'colander': ( + 'http://docs.pylonsproject.org/projects/colander/en/latest', + None), + 'tutorials': ( + 'http://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/', + None), +} diff --git a/docs/quick_tutorial/databases.rst b/docs/quick_tutorial/databases.rst new file mode 100644 index 000000000..d2d2ce699 --- /dev/null +++ b/docs/quick_tutorial/databases.rst @@ -0,0 +1,184 @@ +============================== +19: Databases Using SQLAlchemy +============================== + +Store/retrieve data using the SQLAlchemy ORM atop the SQLite database. + +Background +========== + +Our Pyramid-based wiki application now needs database-backed storage of +pages. This frequently means a SQL database. The Pyramid community +strongly supports the +:ref:`SQLAlchemy ` project and its +:ref:`object-relational mapper (ORM) ` +as a convenient, Pythonic way to interface to databases. + +In this step we hook up SQLAlchemy to a SQLite database table, +providing storage and retrieval for the wikipages in the previous step. + +.. note:: + + The ``alchemy`` scaffold is really helpful for getting a + SQLAlchemy project going, including generation of the console + script. Since we want to see all the decisions, we will forgo + convenience in this tutorial and wire it up ourselves. + +Objectives +========== + +- Store pages in SQLite by using SQLAlchemy models + +- Use SQLAlchemy queries to list/add/view/edit pages + +- Provide a database-initialize command by writing a Pyramid *console + script* which can be run from the command line + +Steps +===== + +#. We are going to use the forms step as our starting point: + + .. code-block:: bash + + (env27)$ cd ..; cp -r forms databases; cd databases + +#. We need to add some dependencies in ``databases/setup.py`` as well + as an "entry point" for the command-line script: + + .. literalinclude:: databases/setup.py + :linenos: + + .. note:: + + We aren't yet doing ``python3.3 setup.py develop`` as we + are changing it later. + +#. Our configuration file at ``databases/development.ini`` wires + together some new pieces: + + .. literalinclude:: databases/development.ini + :language: ini + +#. This engine configuration now needs to be read into the application + through changes in ``databases/tutorial/__init__.py``: + + .. literalinclude:: databases/tutorial/__init__.py + :linenos: + +#. Make a command-line script at ``databases/tutorial/initialize_db.py`` + to initialize the database: + + .. literalinclude:: databases/tutorial/initialize_db.py + +#. Since ``setup.py`` changed, we now run it: + + .. code-block:: bash + + (env27)$ python setup.py develop + +#. The script references some models in ``databases/tutorial/models.py``: + + .. literalinclude:: databases/tutorial/models.py + :linenos: + +#. Let's run this console script, thus producing our database and table: + + .. code-block:: bash + + (env27)$ initialize_tutorial_db development.ini + 2013-09-06 15:54:08,050 INFO [sqlalchemy.engine.base.Engine][MainThread] PRAGMA table_info("wikipages") + 2013-09-06 15:54:08,050 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2013-09-06 15:54:08,051 INFO [sqlalchemy.engine.base.Engine][MainThread] + CREATE TABLE wikipages ( + uid INTEGER NOT NULL, + title TEXT, + body TEXT, + PRIMARY KEY (uid), + UNIQUE (title) + ) + +#. With our data now driven by SQLAlchemy queries, we need to update + our ``databases/tutorial/views.py``: + + .. literalinclude:: databases/tutorial/views.py + +#. Our tests in ``databases/tutorial/tests.py`` changed to include + SQLAlchemy bootstrapping: + + .. literalinclude:: databases/tutorial/tests.py + :linenos: + +#. Run the tests in your package using ``nose``: + + .. code-block:: bash + + (env27)$ nosetests . + .. + ----------------------------------------------------------------- + Ran 2 tests in 1.141s + + OK + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/`` in a browser. + +Analysis +======== + +Let's start with the dependencies. We made the decision to use +``SQLAlchemy`` to talk to our database. We also, though, installed +``pyramid_tm`` and ``zope.sqlalchemy``. Why? + +Pyramid has a strong orientation towards support for ``transactions``. +Specifically, you can install a transaction manager into your app +application, either as middleware or a Pyramid "tween". Then, +just before you return the response, all transaction-aware parts of +your application are executed. + +This means Pyramid view code usually doesn't manage transactions. If +your view code or a template generates an error, the transaction manager +aborts the transaction. This is a very liberating way to write code. + +The ``pyramid_tm`` package provides a "tween" that is configured in the +``development.ini`` configuration file. That installs it. We then need +a package that makes SQLAlchemy and thus the RDBMS transaction manager +integrate with the Pyramid transaction manager. That's what +``zope.sqlalchemy`` does. + +Where do we point at the location on disk for the SQLite file? In the +configuration file. This lets consumers of our package change the +location in a safe (non-code) way. That is, in configuration. This +configuration-oriented approach isn't required in Pyramid; you can +still make such statements in your ``__init__.py`` or some companion +module. + +The ``initialize_tutorial_db`` is a nice example of framework support. +You point your setup at the location of some ``[console_scripts]`` and +these get generated into your virtualenv's ``bin`` directory. Our +console script follows the pattern of being fed a configuration file +with all the bootstrapping. It then opens SQLAlchemy and creates the +root of the wiki, which also makes the SQLite file. Note the +``with transaction.manager`` part that puts the work in the scope of a +transaction (as we aren't inside a web request where this is done +automatically.) + +The ``models.py`` does a little bit extra work to hook up SQLAlchemy +into the Pyramid transaction manager. It then declares the model for a +``Page``. + +Our views have changes primarily around replacing our dummy +dictionary-of-dictionaries data with proper database support: list the +rows, add a row, edit a row, and delete a row. + +Extra Credit +============ + +#. Why all this code? Why can't I just type 2 lines and have magic ensue? + +#. Give a try at a button that deletes a wiki page. diff --git a/docs/quick_tutorial/databases/development.ini b/docs/quick_tutorial/databases/development.ini new file mode 100644 index 000000000..270da960f --- /dev/null +++ b/docs/quick_tutorial/databases/development.ini @@ -0,0 +1,49 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/sqltutorial.sqlite + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial, sqlalchemy + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/databases/setup.py b/docs/quick_tutorial/databases/setup.py new file mode 100644 index 000000000..5cb197c39 --- /dev/null +++ b/docs/quick_tutorial/databases/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'deform', + 'sqlalchemy', + 'pyramid_tm', + 'zope.sqlalchemy', + 'pysqlite' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.initialize_db:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/databases/sqltutorial.sqlite b/docs/quick_tutorial/databases/sqltutorial.sqlite new file mode 100644 index 000000000..b8bd856fd Binary files /dev/null and b/docs/quick_tutorial/databases/sqltutorial.sqlite differ diff --git a/docs/quick_tutorial/databases/tutorial/__init__.py b/docs/quick_tutorial/databases/tutorial/__init__.py new file mode 100644 index 000000000..47e8fefa6 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/__init__.py @@ -0,0 +1,20 @@ +from pyramid.config import Configurator + +from sqlalchemy import engine_from_config + +from .models import DBSession, Base + +def main(global_config, **settings): + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.bind = engine + + config = Configurator(settings=settings, + root_factory='tutorial.models.Root') + config.add_route('wiki_view', '/') + config.add_route('wikipage_add', '/add') + config.add_route('wikipage_view', '/{uid}') + config.add_route('wikipage_edit', '/{uid}/edit') + config.add_static_view('deform_static', 'deform:static/') + config.scan('.views') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/initialize_db.py b/docs/quick_tutorial/databases/tutorial/initialize_db.py new file mode 100644 index 000000000..98be524a1 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/initialize_db.py @@ -0,0 +1,37 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from .models import ( + DBSession, + Page, + Base, + ) + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s \n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) != 2: + usage(argv) + config_uri = argv[1] + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = Page(title='Root', body='

Root

') + DBSession.add(model) diff --git a/docs/quick_tutorial/databases/tutorial/models.py b/docs/quick_tutorial/databases/tutorial/models.py new file mode 100644 index 000000000..b27c38417 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/models.py @@ -0,0 +1,35 @@ +from pyramid.security import Allow, Everyone + +from sqlalchemy import ( + Column, + Integer, + Text, + ) + +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session( + sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class Page(Base): + __tablename__ = 'wikipages' + uid = Column(Integer, primary_key=True) + title = Column(Text, unique=True) + body = Column(Text) + + +class Root(object): + __acl__ = [(Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit')] + + def __init__(self, request): + pass \ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/tests.py b/docs/quick_tutorial/databases/tutorial/tests.py new file mode 100644 index 000000000..e18e70c8c --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/tests.py @@ -0,0 +1,58 @@ +import unittest +import transaction + +from pyramid import testing + + +def _initTestingDB(): + from sqlalchemy import create_engine + from .models import ( + DBSession, + Page, + Base + ) + engine = create_engine('sqlite://') + Base.metadata.create_all(engine) + DBSession.configure(bind=engine) + with transaction.manager: + model = Page(title='FrontPage', body='This is the front page') + DBSession.add(model) + return DBSession + + +class WikiViewTests(unittest.TestCase): + def setUp(self): + self.session = _initTestingDB() + self.config = testing.setUp() + + def tearDown(self): + self.session.remove() + testing.tearDown() + + def test_wiki_view(self): + from tutorial.views import WikiViews + + request = testing.DummyRequest() + inst = WikiViews(request) + response = inst.wiki_view() + self.assertEqual(response['title'], 'Wiki View') + + +class WikiFunctionalTests(unittest.TestCase): + def setUp(self): + self.session = _initTestingDB() + self.config = testing.setUp() + from pyramid.paster import get_app + app = get_app('development.ini') + from webtest import TestApp + self.testapp = TestApp(app) + + def tearDown(self): + self.session.remove() + testing.tearDown() + + def test_it(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'Wiki: View', res.body) + res = self.testapp.get('/add', status=200) + self.assertIn(b'Add/Edit', res.body) diff --git a/docs/quick_tutorial/databases/tutorial/views.py b/docs/quick_tutorial/databases/tutorial/views.py new file mode 100644 index 000000000..4608c6d43 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/views.py @@ -0,0 +1,96 @@ +import colander +import deform.widget + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from .models import DBSession, Page + + +class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + + +class WikiViews(object): + def __init__(self, request): + self.request = request + + @property + def wiki_form(self): + schema = WikiPage() + return deform.Form(schema, buttons=('submit',)) + + @property + def reqts(self): + return self.wiki_form.get_widget_resources() + + @view_config(route_name='wiki_view', renderer='wiki_view.pt') + def wiki_view(self): + pages = DBSession.query(Page).order_by(Page.title) + return dict(title='Wiki View', pages=pages) + + @view_config(route_name='wikipage_add', + renderer='wikipage_addedit.pt') + def wikipage_add(self): + form = self.wiki_form.render() + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = self.wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Form is NOT valid + return dict(form=e.render()) + + # Add a new page to the database + new_title = appstruct['title'] + new_body = appstruct['body'] + DBSession.add(Page(title=new_title, body=new_body)) + + # Get the new ID and redirect + page = DBSession.query(Page).filter_by(title=new_title).one() + new_uid = page.uid + + url = self.request.route_url('wikipage_view', uid=new_uid) + return HTTPFound(url) + + return dict(form=form) + + + @view_config(route_name='wikipage_view', renderer='wikipage_view.pt') + def wikipage_view(self): + uid = int(self.request.matchdict['uid']) + page = DBSession.query(Page).filter_by(uid=uid).one() + return dict(page=page) + + + @view_config(route_name='wikipage_edit', + renderer='wikipage_addedit.pt') + def wikipage_edit(self): + uid = int(self.request.matchdict['uid']) + page = DBSession.query(Page).filter_by(uid=uid).one() + + wiki_form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + return dict(page=page, form=e.render()) + + # Change the content and redirect to the view + page.title = appstruct['title'] + page.body = appstruct['body'] + url = self.request.route_url('wikipage_view', uid=uid) + return HTTPFound(url) + + form = self.wiki_form.render(dict( + uid=page.uid, title=page.title, body=page.body) + ) + + return dict(page=page, form=form) diff --git a/docs/quick_tutorial/databases/tutorial/wiki_view.pt b/docs/quick_tutorial/databases/tutorial/wiki_view.pt new file mode 100644 index 000000000..9e3afe495 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wiki_view.pt @@ -0,0 +1,19 @@ + + + + Wiki: View + + +

Wiki

+ +Add + WikiPage + + + \ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt new file mode 100644 index 000000000..d1fea0d7f --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt @@ -0,0 +1,22 @@ + + + + WikiPage: Add/Edit + + + + + + + + +

Wiki

+ +

${structure: form}

+ + + diff --git a/docs/quick_tutorial/databases/tutorial/wikipage_view.pt b/docs/quick_tutorial/databases/tutorial/wikipage_view.pt new file mode 100644 index 000000000..cb9ff526e --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wikipage_view.pt @@ -0,0 +1,17 @@ + + + + WikiPage: View + + + + Up + | + + Edit + + +

${page.title}

+

${structure: page.body}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/debugtoolbar.rst b/docs/quick_tutorial/debugtoolbar.rst new file mode 100644 index 000000000..42e665d4f --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar.rst @@ -0,0 +1,87 @@ +============================================ +04: Easier Development with ``debugtoolbar`` +============================================ + +Error-handling and introspection using the ``pyramid_debugtoolbar`` +add-on. + +Background +========== + +As we introduce the basics we also want to show how to be productive in +development and debugging. For example, we just discussed template +reloading and earlier we showed ``--reload`` for application reloading. + +``pyramid_debugtoolbar`` is a popular Pyramid add-on which makes +several tools available in your browser. Adding it to your project +illustrates several points about configuration. + +Objectives +========== + +- Install and enable the toolbar to help during development + +- Explain Pyramid add-ons + +- Show how an add-on gets configured into your application + +Steps +===== + +#. First we copy the results of the previous step, as well as install + the ``pyramid_debugtoolbar`` package: + + .. code-block:: bash + + (env27)$ cd ..; cp -r ini debugtoolbar; cd debugtoolbar + (env27)$ python setup.py develop + (env27)$ easy_install pyramid_debugtoolbar + + +#. Our ``debugtoolbar/development.ini`` gets a configuration entry for + ``pyramid.includes``: + + .. literalinclude:: debugtoolbar/development.ini + :language: ini + :linenos: + +#. Run the WSGI application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/`` in your browser. See the handy + toolbar on the right. + +Analysis +======== + +``pyramid_debugtoolbar`` is a full-fledged Python package, +available on PyPI just like thousands of other Python packages. Thus we +start by installing the ``pyramid_debugtoolbar`` package into our +virtual environment using normal Python package installation commands. + +The ``pyramid_debugtoolbar`` Python package is also a Pyramid add-on, +which means we need to include its add-on configuration into our web +application. We could do this with imperative configuration in +``tutorial/__init__.py`` by using ``config.include``. Pyramid also +supports wiring in add-on configuration via our ``development.ini`` +using ``pyramid.includes``. We use this to load the configuration for +the debugtoolbar. + +You'll now see an attractive (and collapsible) menu in the right of +your browser, providing introspective access to debugging information. +Even better, if your web application generates an error, +you will see a nice traceback on the screen. When you want to disable +this toolbar, no need to change code: you can remove it from +``pyramid.includes`` in the relevant ``.ini`` configuration file (thus +showing why configuration files are handy.) + +Note that the toolbar mutates the HTML generated by our app and uses jQuery to +overlay itself. If you are using the toolbar while you're developing and you +start to experience otherwise inexplicable client-side weirdness, you can shut +it off by commenting out the ``pyramid_debugtoolbar`` line in +``pyramid.includes`` temporarily. + +.. seealso:: See Also: :ref:`pyramid_debugtoolbar ` diff --git a/docs/quick_tutorial/debugtoolbar/development.ini b/docs/quick_tutorial/debugtoolbar/development.ini new file mode 100644 index 000000000..470d92c57 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/development.ini @@ -0,0 +1,40 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/debugtoolbar/setup.py b/docs/quick_tutorial/debugtoolbar/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py b/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('

Hello World!

') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst new file mode 100644 index 000000000..bb7cab3f7 --- /dev/null +++ b/docs/quick_tutorial/forms.rst @@ -0,0 +1,146 @@ +==================================== +18: Forms and Validation With Deform +==================================== + +Schema-driven, autogenerated forms with validation. + +Background +========== + +Modern web applications deal extensively with forms. Developers, +though, have a wide range of philosophies about how frameworks should +help them with their forms. As such, Pyramid doesn't directly bundle +one particular form library. Instead, there are a variety of form +libraries that are easy to use in Pyramid. + +:ref:`Deform ` +is one such library. In this step, we introduce Deform for our +forms and validation. This also gives us the +:ref:`Colander ` for schemas and validation. + +Deform is getting a facelift, with styling from Twitter Bootstrap and +advanced widgets from popular JavaScript projects. The work began in +``deform_bootstrap`` and is being merged into an update to Deform. + +Objectives +========== + +- Make a schema using Colander, the companion to Deform + +- Create a form with Deform and change our views to handle validation + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + (env27)$ cd ..; cp -r view_classes forms; cd forms + +#. Let's edit ``forms/setup.py`` to declare a dependency on Deform + (which then pulls in Colander as a dependency: + + .. literalinclude:: forms/setup.py + :linenos: + +#. We can now install our project in development mode: + + .. code-block:: bash + + (env27)$ python setup.py develop + +#. Register a static view in ``forms/tutorial/__init__.py`` for + Deform's CSS/JS etc. as well as our demo wikipage scenario's + views: + + .. literalinclude:: forms/tutorial/__init__.py + :linenos: + +#. Implement the new views, as well as the form schemas and some + dummy data, in ``forms/tutorial/views.py``: + + .. literalinclude:: forms/tutorial/views.py + :linenos: + +#. A template for the top of the "wiki" in + ``forms/tutorial/wiki_view.pt``: + + .. literalinclude:: forms/tutorial/wiki_view.pt + :language: html + :linenos: + +#. Another template for adding/editing in + ``forms/tutorial/wikipage_addedit.pt``: + + .. literalinclude:: forms/tutorial/wikipage_addedit.pt + :language: html + :linenos: + +#. Finally, a template at ``forms/tutorial/wikipage_view.pt`` + for viewing a wiki page: + + .. literalinclude:: forms/tutorial/wikipage_view.pt + :language: html + :linenos: + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/`` in a browser. + + +Analysis +======== + +This step helps illustrate the utility of asset specifications for +static assets. We have an outside package called Deform with static +assets which need to be published. We don't have to know where on disk +it is located. We point at the package, then the path inside the package. + +We just need to include a call to ``add_static_view`` to make that +directory available at a URL. For Pyramid-specific pages, +Pyramid provides a facility (``config.include()``) which even makes +that unnecessary for consumers of a package. (Deform is not specific to +Pyramid.) + +Our forms have rich widgets which need the static CSS and JS just +mentioned. Deform has a :term:`resource registry` which allows widgets +to specify which JS and CSS are needed. Our ``wikipage_addedit.pt`` +template shows how we iterated over that data to generate markup that +includes the needed resources. + +Our add and edit views use a pattern called *self-posting forms*. +Meaning, the same URL is used to ``GET`` the form as is used to +``POST`` the form. The route, the view, and the template are the same +whether you are walking up to it the first time or you clicked a button. + +Inside the view we do ``if 'submit' in self.request.params:`` to see if +this form was a ``POST`` where the user clicked on a particular button +````. + +The form controller then follows a typical pattern: + +- If you are doing a GET, skip over and just return the form + +- If you are doing a POST, validate the form contents + +- If the form is invalid, bail out by re-rendering the form with the + supplied ``POST`` data + +- If the validation succeeded, perform some action and issue a + redirect via ``HTTPFound``. + +We are, in essence, writing our own form controller. Other +Pyramid-based systems, including ``pyramid_deform``, provide a +form-centric view class which automates much of this branching and +routing. + +Extra Credit +============ + +#. Give a try at a button that goes to a delete view for a + particular wiki page. diff --git a/docs/quick_tutorial/forms/development.ini b/docs/quick_tutorial/forms/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/forms/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/forms/setup.py b/docs/quick_tutorial/forms/setup.py new file mode 100644 index 000000000..5db620eb9 --- /dev/null +++ b/docs/quick_tutorial/forms/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'deform' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/__init__.py b/docs/quick_tutorial/forms/tutorial/__init__.py new file mode 100644 index 000000000..b12a4154a --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/__init__.py @@ -0,0 +1,12 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('wiki_view', '/') + config.add_route('wikipage_add', '/add') + config.add_route('wikipage_view', '/{uid}') + config.add_route('wikipage_edit', '/{uid}/edit') + config.add_static_view('deform_static', 'deform:static/') + config.scan('.views') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/forms/tutorial/tests.py b/docs/quick_tutorial/forms/tutorial/tests.py new file mode 100644 index 000000000..6ff554a1e --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/tests.py @@ -0,0 +1,47 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def tearDown(self): + testing.tearDown() + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'

Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'

Hi Hello View', res.body) diff --git a/docs/quick_tutorial/forms/tutorial/views.py b/docs/quick_tutorial/forms/tutorial/views.py new file mode 100644 index 000000000..004d2aba9 --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/views.py @@ -0,0 +1,96 @@ +import colander +import deform.widget + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +pages = { + '100': dict(uid='100', title='Page 100', body='100'), + '101': dict(uid='101', title='Page 101', body='101'), + '102': dict(uid='102', title='Page 102', body='102') +} + +class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + + +class WikiViews(object): + def __init__(self, request): + self.request = request + + @property + def wiki_form(self): + schema = WikiPage() + return deform.Form(schema, buttons=('submit',)) + + @property + def reqts(self): + return self.wiki_form.get_widget_resources() + + @view_config(route_name='wiki_view', renderer='wiki_view.pt') + def wiki_view(self): + return dict(pages=pages.values()) + + @view_config(route_name='wikipage_add', + renderer='wikipage_addedit.pt') + def wikipage_add(self): + form = self.wiki_form.render() + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = self.wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Form is NOT valid + return dict(form=e.render()) + + # Form is valid, make a new identifier and add to list + last_uid = int(sorted(pages.keys())[-1]) + new_uid = str(last_uid + 1) + pages[new_uid] = dict( + uid=new_uid, title=appstruct['title'], + body=appstruct['body'] + ) + + # Now visit new page + url = self.request.route_url('wikipage_view', uid=new_uid) + return HTTPFound(url) + + return dict(form=form) + + @view_config(route_name='wikipage_view', renderer='wikipage_view.pt') + def wikipage_view(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + return dict(page=page) + + @view_config(route_name='wikipage_edit', + renderer='wikipage_addedit.pt') + def wikipage_edit(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + + wiki_form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + return dict(page=page, form=e.render()) + + # Change the content and redirect to the view + page['title'] = appstruct['title'] + page['body'] = appstruct['body'] + + url = self.request.route_url('wikipage_view', + uid=page['uid']) + return HTTPFound(url) + + form = wiki_form.render(page) + + return dict(page=page, form=form) \ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/wiki_view.pt b/docs/quick_tutorial/forms/tutorial/wiki_view.pt new file mode 100644 index 000000000..9e3afe495 --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wiki_view.pt @@ -0,0 +1,19 @@ + + + + Wiki: View + + +

Wiki

+ +Add + WikiPage + + + \ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt new file mode 100644 index 000000000..d1fea0d7f --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt @@ -0,0 +1,22 @@ + + + + WikiPage: Add/Edit + + + + + + + + +

Wiki

+ +

${structure: form}

+ + + diff --git a/docs/quick_tutorial/forms/tutorial/wikipage_view.pt b/docs/quick_tutorial/forms/tutorial/wikipage_view.pt new file mode 100644 index 000000000..cb9ff526e --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wikipage_view.pt @@ -0,0 +1,17 @@ + + + + WikiPage: View + + + + Up + | + + Edit + + +

${page.title}

+

${structure: page.body}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing.rst b/docs/quick_tutorial/functional_testing.rst new file mode 100644 index 000000000..777cb01e2 --- /dev/null +++ b/docs/quick_tutorial/functional_testing.rst @@ -0,0 +1,68 @@ +=================================== +06: Functional Testing with WebTest +=================================== + +Write end-to-end full-stack testing using ``webtest``. + +Background +========== + +Unit tests are a common and popular approach to test-driven development +(TDD.) In web applications, though, the templating and entire apparatus +of a web site are important parts of the delivered quality. We'd like a +way to test these. + +WebTest is a Python package that does functional testing. With WebTest +you can write tests which simulate a full HTTP request against a WSGI +application, then test the information in the response. For speed +purposes, WebTest skips the setup/teardown of an actual HTTP server, +providing tests that run fast enough to be part of TDD. + +Objectives +========== + +- Write a test which checks the contents of the returned HTML + +Steps +===== + +#. First we copy the results of the previous step, as well as install + the ``webtest`` package: + + .. code-block:: bash + + (env27)$ cd ..; cp -r unit_testing functional_testing; cd functional_testing + (env27)$ python setup.py develop + (env27)$ easy_install webtest + +#. Let's extend ``unit_testing/tutorial/tests.py`` to include a + functional test: + + .. literalinclude:: functional_testing/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + (env27)$ nosetests tutorial + . + ---------------------------------------------------------------------- + Ran 2 tests in 0.141s + + OK + +Analysis +======== + +We now have the end-to-end testing we were looking for. WebTest lets us +simply extend our existing ``nose``-based test approach with functional +tests that are reported in the same output. These new tests not only +cover our templating, but they didn't dramatically increase the +execution time of our tests. + +Extra Credit +============ + +#. Why do our functional tests use ``b''``? \ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing/development.ini b/docs/quick_tutorial/functional_testing/development.ini new file mode 100644 index 000000000..470d92c57 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/development.ini @@ -0,0 +1,40 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/functional_testing/setup.py b/docs/quick_tutorial/functional_testing/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing/tutorial/__init__.py b/docs/quick_tutorial/functional_testing/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('

Hello World!

') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing/tutorial/tests.py b/docs/quick_tutorial/functional_testing/tutorial/tests.py new file mode 100644 index 000000000..4248acbe7 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/tutorial/tests.py @@ -0,0 +1,31 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_hello_world(self): + from tutorial import hello_world + + request = testing.DummyRequest() + response = hello_world(request) + self.assertEqual(response.status_code, 200) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_hello_world(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'

Hello World!

', res.body) diff --git a/docs/quick_tutorial/hello_world.rst b/docs/quick_tutorial/hello_world.rst new file mode 100644 index 000000000..e44ef616d --- /dev/null +++ b/docs/quick_tutorial/hello_world.rst @@ -0,0 +1,111 @@ +================================ +01: Single-File Web Applications +================================ + +What's the simplest way to get started in Pyramid? A single-file module. +No Python packages, no ``setup.py``, no other machinery. + +Background +========== + +Microframeworks are all the rage these days. "Microframework" is a +marketing term, not a technical one. They have a low mental overhead: +they do so little, the only things you have to worry about are *your +things*. + +Pyramid is special because it can act as a single-file module +microframework. You can have a single Python file that can be executed +directly by Python. But Pyramid also provides facilities to scale to +the largest of applications. + +Python has a standard called :term:`WSGI` that defines how +Python web applications plug into standard servers, getting passed +incoming requests and returning responses. Most modern Python web +frameworks obey an "MVC" (model-view-controller) application pattern, +where the data in the model has a view that mediates interaction with +outside systems. + +In this step we'll see a brief glimpse of WSGI servers, WSGI +applications, requests, responses, and views. + +Objectives +========== + +- Get a running Pyramid web application, as simply as possible + +- Use that as a well-understood base for adding each unit of complexity + +- Initial exposure to WSGI apps, requests, views, and responses + +Steps +===== + +#. Make sure you have followed the steps in :doc:`python_setup`. + +#. Create a directory for this step: + + .. code-block:: bash + + (env27)$ mkdir hello_world; cd hello_world + +#. Copy the following into ``hello_world/app.py``: + + .. literalinclude:: hello_world/app.py + :linenos: + +#. Run the application: + + .. code-block:: bash + + (env27)$ python app.py + +#. Open ``http://localhost:6543/`` in your browser. + +Analysis +======== + +New to Python web programming? If so, some lines in module merit +explanation: + +#. *Line 11*. The ``if __name__ == '__main__':`` is Python's way of + saying "Start here when running from the command line". + +#. *Lines 12-14*. Use Pyramid's :term:`pyramid:configurator` to connect + :term:`pyramid:view` code to a particular URL + :term:`pyramid:route`. + +#. *Lines 6-7*. Implement the view code that generates the + :term:`pyramid:response`. + +#. *Lines 15-17*. Publish a :term:`pyramid:WSGI` app using an HTTP + server. + +As shown in this example, the :term:`pyramid:configurator` plays a +central role in Pyramid development. Building an application from +loosely-coupled parts via :ref:`pyramid:configuration_narr` is a +central idea in Pyramid, one that we will revisit regularly in this +*Quick Tour*. + +Extra Credit +============ + +#. Why do we do this: + + .. code-block:: python + + print ('Starting up server on http://localhost:6547') + + ...instead of: + + .. code-block:: python + + print 'Starting up server on http://localhost:6547' + +#. What happens if you return a string of HTML? A sequence of integers? + +#. Put something invalid, such as ``print xyz``, in the view function. + Kill your ``python app.py`` with ``cntrl-c`` and restart, + then reload your browser. See the exception in the console? + +#. The ``GI`` in ``WSGI`` stands for "Gateway Interface". What web + standard is this modelled after? \ No newline at end of file diff --git a/docs/quick_tutorial/hello_world/app.py b/docs/quick_tutorial/hello_world/app.py new file mode 100644 index 000000000..210075023 --- /dev/null +++ b/docs/quick_tutorial/hello_world/app.py @@ -0,0 +1,17 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + print ('Incoming request') + return Response('

Hello World!

') + + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() \ No newline at end of file diff --git a/docs/quick_tutorial/index.rst b/docs/quick_tutorial/index.rst new file mode 100644 index 000000000..9d32f9a81 --- /dev/null +++ b/docs/quick_tutorial/index.rst @@ -0,0 +1,51 @@ +.. _quick_tutorial: + +========================== +Quick Tutorial for Pyramid +========================== + +Pyramid is a web framework for Python 2 and 3. This tutorial gives a +Python 2/3-compatible, high-level tour of the major features. + +This hands-on tutorial covers "a little about a lot": practical +introductions to the most common facilities. Fun, fast-paced, and most +certainly not aimed at experts of the Pyramid web framework. + +Contents +======== + +.. toctree:: + :maxdepth: 1 + + python_setup + pyramid_setup + tutorial_approach + scaffolds + hello_world + package + ini + debugtoolbar + unit_testing + functional_testing + views + templating + view_classes + request_response + routing + jinja2 + static_assets + json + more_view_classes + logging + sessions + forms + databases + authentication + authorization + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst new file mode 100644 index 000000000..85b07812e --- /dev/null +++ b/docs/quick_tutorial/ini.rst @@ -0,0 +1,144 @@ +================================================= +03: Application Configuration with ``.ini`` Files +================================================= + +Use Pyramid's ``pserve`` command with a ``.ini`` configuration file for +simpler, better application running. + +Background +========== + +Pyramid has a first-class concept of +:ref:`configuration ` distinct from code. +This approach is optional, but its presence makes it distinct from +other Python web frameworks. It taps into Python's ``setuptools`` +library, which establishes conventions for how Python projects can be +installed and provide "entry points". Pyramid uses an entry point to +let a Pyramid application it where to find the WSGI app. + +Objectives +========== + +- Modify our ``setup.py`` to have an entry point telling Pyramid the + location of the WSGI app + +- Create an application driven by a ``.ini`` file + +- Startup the application with Pyramid's ``pserve`` command + +- Move code into the package's ``__init__.py`` + +Steps +===== + +#. First we copy the results of the previous step: + + .. code-block:: bash + + (env27)$ cd ..; cp -r package ini; cd ini + +#. Our ``ini/setup.py`` needs a setuptools "entry point" in the + ``setup()`` function: + + .. literalinclude:: ini/setup.py + :linenos: + +#. We can now install our project, thus generating (or re-generating) an + "egg" at ``ini/tutorial.egg-info``: + + .. code-block:: bash + + (env27)$ python setup.py develop + +#. Let's make a file ``ini/development.ini`` for our configuration: + + .. literalinclude:: ini/development.ini + :language: ini + :linenos: + +#. We can refactor our startup code from the previous step's ``app.py`` + into ``ini/tutorial/__init__.py``: + + .. literalinclude:: ini/tutorial/__init__.py + :linenos: + +#. Now that ``ini/tutorial/app.py`` isn't used, let's remove it: + + .. code-block:: bash + + (env27)$ rm tutorial/app.py + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/``. + +Analysis +======== + +Our ``development.ini`` file is read by ``pserve`` and serves to +bootstrap our application. Processing then proceeds as described in +the Pyramid chapter on +:ref:`application startup `: + +- ``pserve`` looks for ``[app:main]`` and finds ``use = egg:tutorial`` + +- The projects's ``setup.py`` has defined an "entry point" (lines 9-10) + for the project "main" entry point of ``tutorial:main`` + +- The ``tutorial`` package's ``__init__`` has a ``main`` function + +- This function is invoked, with the values from certain ``.ini`` + sections passed in + +The ``.ini`` file is also used for two other functions: + +- *Choice of WSGI server*. ``[server:main]`` wires up the choice of WSGI + *server* for your WSGI *application*. In this case, we are using + ``wsgiref`` bundled in the Python library. + +- *Python logging*. Pyramid uses Python standard logging, which needs a + number of configuration values. The ``.ini`` serves this function. + This provides the console log output that you see on startup and each + request. + +- *Port number*. ``port = 6543`` tells ``wsgiref`` to listen on port + 6543. + +We moved our startup code from ``app.py`` to the package's +``tutorial/__init__.py``. This isn't necessary, +but it is a common style in Pyramid to take the WSGI app bootstrapping +out of your module's code and put it in the package's ``__init__.py``. + +The ``pserve`` application runner has a number of command-line arguments +and options. We are using ``--reload`` which tells ``pserve`` to watch +the filesystem for changes to relevant code (Python files, the INI file, +etc.) and, when something changes, restart the application. Very handy +during development. + +Extra Credit +============ + +#. If you don't like configuration and/or ``.ini`` files, + could you do this yourself in Python code? + +#. Can we have multiple ``.ini`` configuration files for a project? Why + might you want to do that? + +#. The entry point in ``setup.py`` didn't mention ``__init__.py`` when + it the ``main`` function. Why not? + +.. seealso:: + :ref:`pyramid:project_narr`, + :ref:`pyramid:scaffolding_chapter`, + :ref:`pyramid:what_is_this_pserve_thing`, + :ref:`pyramid:environment_chapter`, + :ref:`pyramid:paste_chapter` + +Extra Credit +============ + +#. What is the purpose of ``**settings``? What does the ``**`` signify? diff --git a/docs/quick_tutorial/ini/development.ini b/docs/quick_tutorial/ini/development.ini new file mode 100644 index 000000000..ca7d9bf81 --- /dev/null +++ b/docs/quick_tutorial/ini/development.ini @@ -0,0 +1,38 @@ +[app:main] +use = egg:tutorial + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/ini/setup.py b/docs/quick_tutorial/ini/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/ini/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/ini/tutorial/__init__.py b/docs/quick_tutorial/ini/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/ini/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('

Hello World!

') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst new file mode 100644 index 000000000..ad5b8a9ca --- /dev/null +++ b/docs/quick_tutorial/jinja2.rst @@ -0,0 +1,96 @@ +============================== +12: Templating With ``jinja2`` +============================== + +We just said Pyramid doesn't prefer one templating language over +another. Time to prove it. Jinja2 is a popular templating system, +used in Flask and modelled after Django's templates. Let's add +``pyramid_jinja2``, a Pyramid :term:`add-on` which enables Jinja2 as a +:term:`renderer` in our Pyramid applications. + +Objectives +========== + +- Show Pyramid's support for different templating systems + +- Learn about installing Pyramid add-ons + +Steps +===== + +#. In this step let's start by installing the ``pyramid_jinja2`` + add-on, the copying the ``view_class`` step's directory: + + .. code-block:: bash + + (env27)$ cd ..; cp -r view_classes jinja2; cd jinja2 + (env27)$ python setup.py develop + (env27)$ easy_install pyramid_jinja2 + +#. We need to add an item to ``pyramid.includes`` in + ``jinja2/development.ini``: + + .. literalinclude:: jinja2/development.ini + :language: ini + :linenos: + +#. Our ``jinja2/tutorial/views.py`` simply changes its ``renderer``: + + .. literalinclude:: jinja2/tutorial/views.py + :linenos: + +#. Add ``jinja2/tutorial/home.jinja2`` as a template: + + .. literalinclude:: jinja2/tutorial/home.jinja2 + :language: html + +#. Get the ``pyramid.includes`` into the functional test setup in + ``jinja2/tutorial/tests.py``: + + .. literalinclude:: jinja2/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + (env27)$ nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/`` in your browser. + +Analysis +======== + +Getting a Pyramid add-on into Pyramid is simple. First you use normal +Python package installation tools to install the add-on package into +your Python. You then tell Pyramid's configurator to run the setup code +in the add-on. In this case the setup code told Pyramid to make a new +"renderer" available that looked for ``.jinja2`` file extensions. + +Our view code stayed largely the same. We simply changed the file +extension on the renderer. For the template, the syntax for Chameleon +and Jinja2's basic variable insertion is very similar. + +Our functional tests don't have ``development.ini`` so they needed the +``pyramid.includes`` to be setup in the test setup. + +Extra Credit +============ + +#. Our project now depends on ``pyramid_jinja2``. We installed that + dependency manually. What is another way we could have made the + association? + +#. We used ``development.ini`` to get the :term:`configurator` to + load ``pyramid_jinja2``'s configuration. What is another way could + include it into the config? + +.. seealso:: `Jinja2 homepage `_, + and + :ref:`pyramid_jinja2 Overview ` diff --git a/docs/quick_tutorial/jinja2/development.ini b/docs/quick_tutorial/jinja2/development.ini new file mode 100644 index 000000000..c096fa936 --- /dev/null +++ b/docs/quick_tutorial/jinja2/development.ini @@ -0,0 +1,42 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + pyramid_jinja2 + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/jinja2/setup.py b/docs/quick_tutorial/jinja2/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/jinja2/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/__init__.py b/docs/quick_tutorial/jinja2/tutorial/__init__.py new file mode 100644 index 000000000..013d4538f --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/home.jinja2 b/docs/quick_tutorial/jinja2/tutorial/home.jinja2 new file mode 100644 index 000000000..975323169 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/home.jinja2 @@ -0,0 +1,9 @@ + + + + Quick Tour: {{ name }} + + +

Hi {{ name }}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/home.pt b/docs/quick_tutorial/jinja2/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/home.pt @@ -0,0 +1,9 @@ + + + + Quick Tour: ${name} + + +

Hi ${name}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/tests.py b/docs/quick_tutorial/jinja2/tutorial/tests.py new file mode 100644 index 000000000..0b22946f3 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/tests.py @@ -0,0 +1,50 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + settings = { + 'pyramid.includes': [ + 'pyramid_jinja2' + ] + } + app = main({}, **settings) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'

Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'

Hi Hello View', res.body) diff --git a/docs/quick_tutorial/jinja2/tutorial/views.py b/docs/quick_tutorial/jinja2/tutorial/views.py new file mode 100644 index 000000000..fa9ec5121 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/views.py @@ -0,0 +1,18 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.jinja2') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/json.rst b/docs/quick_tutorial/json.rst new file mode 100644 index 000000000..213de8364 --- /dev/null +++ b/docs/quick_tutorial/json.rst @@ -0,0 +1,101 @@ +======================================== +14: Ajax Development With JSON Renderers +======================================== + +Modern web apps are more than rendered HTML. Dynamic pages now use +JavaScript to update the UI in the browser by requesting server data as +JSON. Pyramid supports this with a *JSON renderer*. + +Background +========== + +As we saw in :doc:`templating`, view declarations can specify a +renderer. Output from the view is then run through the renderer, +which generates and returns the ``Response``. We first used a Chameleon +renderer, then a Jinja2 renderer. + +Renderers aren't limited, however, to templates that generate HTML. +Pyramid supplies a JSON renderer which takes Python data, +serializes it to JSON, and performs some other functions such as +setting the content type. In fact, you can write your own renderer (or +extend a built-in renderer) containing custom logic for your unique +application. + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + (env27)$ cd ..; cp -r view_classes json; cd json + (env27)$ python setup.py develop + +#. We add a new route for ``hello_json`` in + ``json/tutorial/__init__.py``: + + .. literalinclude:: json/tutorial/__init__.py + :linenos: + +#. Rather than implement a new view, we will "stack" another decorator + on the ``hello`` view: + + .. literalinclude:: json/tutorial/views.py + :linenos: + +#. We need a new functional test at the end of + ``json/tutorial/tests.py``: + + .. literalinclude:: json/tutorial/tests.py + :linenos: + +#. Run the tests: + + .. code-block:: bash + + (env27)$ nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/howdy.json`` in your browser and you + will see the resulting JSON response. + +Analysis +======== + +Earlier we changed our view functions and methods to return Python +data. This change to a data-oriented view layer made test writing +easier, decoupling the templating from the view logic. + +Since Pyramid has a JSON renderer as well as the templating renderers, +it is an easy step to return JSON. In this case we kept the exact same +view and arranged to return a JSON encoding of the view data. We did +this by: + +- Adding a route to map ``/howdy.json`` to a route name + +- Providing a ``@view_config`` that associated that route name with an + existing view + +- *overriding* the view defaults in the view config that mentions the + ``hello_json`` route, so that when the route is matched, we use the JSON + renderer rather than the ``home.pt`` template renderer that would otherwise + be used. + +In fact, for pure Ajax-style web applications, we could re-use the existing +route by using Pyramid's view predicates to match on the +``Accepts:`` header sent by modern Ajax implementation. + +Pyramid's JSON renderer uses the base Python JSON encoder, +thus inheriting its strengths and weaknesses. For example, +Python can't natively JSON encode DateTime objects. There are a number +of solutions for this in Pyramid, including extending the JSON renderer +with a custom renderer. + +.. seealso:: :ref:`pyramid:views_which_use_a_renderer`, + :ref:`pyramid:json_renderer`, and + :ref:`pyramid:adding_and_overriding_renderers` diff --git a/docs/quick_tutorial/json/development.ini b/docs/quick_tutorial/json/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/json/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/json/setup.py b/docs/quick_tutorial/json/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/json/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/json/tutorial/__init__.py b/docs/quick_tutorial/json/tutorial/__init__.py new file mode 100644 index 000000000..61ddb5129 --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('hello_json', 'howdy.json') + config.scan('.views') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/json/tutorial/home.pt b/docs/quick_tutorial/json/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/home.pt @@ -0,0 +1,9 @@ + + + + Quick Tour: ${name} + + +

Hi ${name}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/json/tutorial/tests.py b/docs/quick_tutorial/json/tutorial/tests.py new file mode 100644 index 000000000..c3cdacbdb --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/tests.py @@ -0,0 +1,50 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'

Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'

Hi Hello View', res.body) + + def test_hello_json(self): + res = self.testapp.get('/howdy.json', status=200) + self.assertIn(b'{"name": "Hello View"}', res.body) + self.assertEqual(res.content_type, 'application/json') + diff --git a/docs/quick_tutorial/json/tutorial/views.py b/docs/quick_tutorial/json/tutorial/views.py new file mode 100644 index 000000000..f15e55d1b --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/views.py @@ -0,0 +1,19 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + @view_config(route_name='hello_json', renderer='json') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/logging.rst b/docs/quick_tutorial/logging.rst new file mode 100644 index 000000000..a81b961b0 --- /dev/null +++ b/docs/quick_tutorial/logging.rst @@ -0,0 +1,77 @@ +============================================ +16: Collecting Application Info With Logging +============================================ + +Capture debugging and error output from your web applications using +standard Python logging. + +Background +========== + +It's important to know what is going on inside our web application. +In development we might need to collect some output. In production, +we might need to detect problems when other people use the site. We +need *logging*. + +Fortunately Pyramid uses the normal Python approach to logging. The +scaffold generated, in your ``development.ini``, a number of lines that +configure the logging for you to some reasonable defaults. You then see +messages sent by Pyramid (for example, when a new request comes in.) + +Objectives +========== + +- Inspect the configuration setup used for logging + +- Add logging statements to your view code + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + (env27)$ cd ..; cp -r view_classes logging; cd logging + (env27)$ python setup.py develop + +#. Extend ``logging/tutorial/views.py`` to log a message: + + .. literalinclude:: logging/tutorial/views.py + :linenos: + +#. Make sure the tests still pass: + + .. code-block:: bash + + (env27)$ nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/`` and ``http://localhost:6543/howdy`` + in your browser. Note, both in the console and in the debug + toolbar, the message that you logged. + +Analysis +======== + +Our ``development.ini`` configuration file wires up Python standard +logging for our Pyramid application: + +.. literalinclude:: logging/development.ini + :language: ini + +In this, our ``tutorial`` Python package is setup as a logger +and configured to log messages at a ``DEBUG`` or higher level. When you +visit ``http://localhost:6543`` your console will now show:: + + 2013-08-09 10:42:42,968 DEBUG [tutorial.views][MainThread] In home view + +Also, if you have configured your Pyramid application to use the +``pyramid_debugtoolbar``, logging statements appear in one of its menus. + +.. seealso:: See Also: :ref:`pyramid:logging_chapter` diff --git a/docs/quick_tutorial/logging/development.ini b/docs/quick_tutorial/logging/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/logging/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/logging/setup.py b/docs/quick_tutorial/logging/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/logging/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/logging/tutorial/__init__.py b/docs/quick_tutorial/logging/tutorial/__init__.py new file mode 100644 index 000000000..013d4538f --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/logging/tutorial/home.pt b/docs/quick_tutorial/logging/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/home.pt @@ -0,0 +1,9 @@ + + + + Quick Tour: ${name} + + +

Hi ${name}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/logging/tutorial/tests.py b/docs/quick_tutorial/logging/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'

Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'

Hi Hello View', res.body) diff --git a/docs/quick_tutorial/logging/tutorial/views.py b/docs/quick_tutorial/logging/tutorial/views.py new file mode 100644 index 000000000..63d95f405 --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/views.py @@ -0,0 +1,23 @@ +import logging +log = logging.getLogger(__name__) + +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + log.debug('In home view') + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + log.debug('In hello view') + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/more_view_classes.rst b/docs/quick_tutorial/more_view_classes.rst new file mode 100644 index 000000000..b01e17a67 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes.rst @@ -0,0 +1,180 @@ +========================== +15: More With View Classes +========================== + +Group views into a class, sharing configuration, state, and logic. + +Background +========== + +As part of its mission to help build more ambitious web applications, +Pyramid provides many more features for views and view classes. + +The Pyramid documentation discusses views as a Python "callable". This +callable can be a function, an object with an ``__call__``, +or a Python class. In this last case, methods on the class can be +decorated with ``@view_config`` to register the class methods with the +:term:`configurator` as a view. + +So far our views have been simple, free-standing functions. Many times +your views are related: different ways to look at or work on the same +data or a REST API that handles multiple operations. Grouping these +together as a +:ref:`view class ` makes sense: + +- Group views + +- Centralize some repetitive defaults + +- Share some state and helpers + +Pyramid views have +:ref:`view predicates ` that +help determine which view is matched to a request. These predicates +provide many axes of flexibility. + +The following shows a simple example with four operations operations: +view a home page which leads to a form, save a change, +and press the delete button. + +Objectives +========== + +- Group related views into a view class + +- Centralize configuration with class-level ``@view_defaults`` + +- Dispatch one route/URL to multiple views based on request data + +- Share stated and logic between views and templates via the view class + +Steps +===== + +#. First we copy the results of the previous step: + + .. code-block:: bash + + (env27)$ cd ..; cp -r templating more_view_classes; cd more_view_classes + (env27)$ python setup.py develop + +#. Our route in ``more_view_classes/tutorial/__init__.py`` needs some + replacement patterns: + + .. literalinclude:: more_view_classes/tutorial/__init__.py + :linenos: + +#. Our ``more_view_classes/tutorial/views.py`` now has a view class with + several views: + + .. literalinclude:: more_view_classes/tutorial/views.py + :linenos: + +#. Our primary view needs a template at + ``more_view_classes/tutorial/home.pt``: + + .. literalinclude:: more_view_classes/tutorial/home.pt + :language: html + +#. Ditto for our other view from the previous section at + ``more_view_classes/tutorial/hello.pt``: + + .. literalinclude:: more_view_classes/tutorial/hello.pt + :language: html + +#. We have an edit view that also needs a template at + ``more_view_classes/tutorial/edit.pt``: + + .. literalinclude:: more_view_classes/tutorial/edit.pt + :language: html + +#. And finally the delete view's template at + ``more_view_classes/tutorial/delete.pt``: + + .. literalinclude:: more_view_classes/tutorial/delete.pt + :language: html + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/howdy/jane/doe`` in your browser. Click + the ``Save`` and ``Delete`` buttons and watch the output in the + console window. + +Analysis +======== + +As you can see, the four views are logically grouped together. +Specifically: + +- We have a ``home`` view available at ``http://localhost:6543/`` with + a clickable link to the ``hello`` view. + +- The second view is returned when you go to ``/howdy/jane/doe``. This + URL is + mapped to the ``hello`` route that we centrally set using the optional + ``@view_defaults``. + +- The third view is returned when the form is submitted with a ``POST`` + method. This rule is specified in the ``@view_config`` for that view. + +- The fourth view is returned when clicking on a button such + as ````. + +In this step we show using the following information as criteria to +decide which view to use: + +- Method of the HTTP request (``GET``, ``POST``, etc.) + +- Parameter information in the request (submitted form field names) + +We also centralize part of the view configuration to the class level +with ``@view_defaults``, then in one view, override that default just +for that one view. Finally, we put this commonality between views to +work in the view class by sharing: + +- State assigned in ``TutorialViews.__init__`` + +- A computed value + +These are then available both in the view methods but also in the +templates (e.g. ``${view.view_name}`` and ``${view.full_name}``. + +As a note, we made a switch in our templates on how we generate URLs. +We previously hardcode the URLs, such as:: + + Howdy + +In ``home.pt`` we switched to:: + + form + +Pyramid has rich facilities to help generate URLs in a flexible, +non-error-prone fashion. + +Extra Credit +============ + +#. Why could our template do ``${view.full_name}`` and not have to do + ``${view.full_name()}``? + +#. The ``edit`` and ``delete`` views are both submitted to with + ``POST``. Why does the ``edit`` view configuration not catch the + the ``POST`` used by ``delete``? + +#. We used Python ``@property`` on ``full_name``. If we reference this + many times in a template or view code, it would re-compute this + every time. Does Pyramid provide something that will cache the initial + computation on a property? + +#. Can you associate more than one route with the same view? + +#. There is also a ``request.route_path`` API. How does this differ from + ``request.route_url``? + +.. seealso:: :ref:`pyramid:class_as_view`, `Weird Stuff You Can Do With + URL Dispatch `_ diff --git a/docs/quick_tutorial/more_view_classes/development.ini b/docs/quick_tutorial/more_view_classes/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/more_view_classes/setup.py b/docs/quick_tutorial/more_view_classes/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/__init__.py b/docs/quick_tutorial/more_view_classes/tutorial/__init__.py new file mode 100644 index 000000000..4ab83b413 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('hello', '/howdy/{first}/{last}') + config.scan('.views') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/delete.pt b/docs/quick_tutorial/more_view_classes/tutorial/delete.pt new file mode 100644 index 000000000..67cc8bf09 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/delete.pt @@ -0,0 +1,9 @@ + + + + Quick Tour: ${page_title} + + +

${view.view_name} - ${page_title}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/edit.pt b/docs/quick_tutorial/more_view_classes/tutorial/edit.pt new file mode 100644 index 000000000..1bd204065 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/edit.pt @@ -0,0 +1,10 @@ + + + + Quick Tour: ${view.view_name} - ${page_title} + + +

${view.view_name} - ${page_title}

+

You submitted ${new_name}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/hello.pt b/docs/quick_tutorial/more_view_classes/tutorial/hello.pt new file mode 100644 index 000000000..8a39aed09 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/hello.pt @@ -0,0 +1,16 @@ + + + + Quick Tour: ${view.view_name} - ${page_title} + + +

${view.view_name} - ${page_title}

+

Welcome, ${view.full_name}

+
+ + + +
+ + \ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/home.pt b/docs/quick_tutorial/more_view_classes/tutorial/home.pt new file mode 100644 index 000000000..fa9016705 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/home.pt @@ -0,0 +1,12 @@ + + + + Quick Tour: ${view.view_name} - ${page_title} + + +

${view.view_name} - ${page_title}

+ +

Go to the form.

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/tests.py b/docs/quick_tutorial/more_view_classes/tutorial/tests.py new file mode 100644 index 000000000..dca8d7f7b --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/tests.py @@ -0,0 +1,31 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['page_title']) + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'TutorialViews - Home View', res.body) diff --git a/docs/quick_tutorial/more_view_classes/tutorial/views.py b/docs/quick_tutorial/more_view_classes/tutorial/views.py new file mode 100644 index 000000000..fdba04ba8 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/views.py @@ -0,0 +1,39 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(route_name='hello') +class TutorialViews: + def __init__(self, request): + self.request = request + self.view_name = 'TutorialViews' + + @property + def full_name(self): + first = self.request.matchdict['first'] + last = self.request.matchdict['last'] + return first + ' ' + last + + @view_config(route_name='home', renderer='home.pt') + def home(self): + return {'page_title': 'Home View'} + + + # Retrieving /howdy/first/last the first time + @view_config(renderer='hello.pt') + def hello(self): + return {'page_title': 'Hello View'} + + # Posting to /home via the "Edit" submit button + @view_config(request_method='POST', renderer='edit.pt') + def edit(self): + new_name = self.request.params['new_name'] + return {'page_title': 'Edit View', 'new_name': new_name} + + # Posting to /home via the "Delete" submit button + @view_config(request_param='form.delete', renderer='delete.pt') + def delete(self): + print ('Deleted') + return {'page_title': 'Delete View'} diff --git a/docs/quick_tutorial/package.rst b/docs/quick_tutorial/package.rst new file mode 100644 index 000000000..da6624cb1 --- /dev/null +++ b/docs/quick_tutorial/package.rst @@ -0,0 +1,112 @@ +============================================ +02: Python Packages for Pyramid Applications +============================================ + +Most modern Python development is done using Python packages, an approach +Pyramid puts to good use. In this step we re-do "Hello World" as a +minimum Python package inside a minimum Python project. + +Background +========== + +Python developers can organize a collection of modules and files into a +namespaced unit called a :ref:`package `. If a +directory is on ``sys.path`` and has a special file named +``__init__.py``, it is treated as a Python package. + +Packages can be bundled up, made available for installation, +and installed through a (muddled, but improving) toolchain oriented +around a ``setup.py`` file for a +`setuptools project `_. +Explaining it all in this +tutorial will induce madness. For this tutorial, this is all you need to +know: + +- We will have a directory for each tutorial step as a + setuptools *project* + +- This project will contain a ``setup.py`` which injects the features + of the setuptool's project machinery into the directory + +- In this project we will make a ``tutorial`` subdirectory into a Python + *package* using an ``__init__.py`` Python module file + +- We will run ``python setup.py develop`` to install our project in + development mode + +In summary: + +- You'll do your development in a Python *package* + +- That package will be part of a setuptools *project* + +Objectives +========== + +- Make a Python "package" directory with an ``__init__.py`` + +- Get a minimum Python "project" in place by making a ``setup.py`` + +- Install our ``tutorial`` project in development mode + +Steps +===== + +#. Make an area for this tutorial step: + + .. code-block:: bash + + (env27)$ cd ..; mkdir package; cd package + +#. In ``package/setup.py``, enter the following: + + .. literalinclude:: package/setup.py + +#. Make the new project installed for development then make a directory + for the actual code: + + .. code-block:: bash + + (env27)$ python setup.py develop + (env27)$ mkdir tutorial + +#. Enter the following into ``package/tutorial/__init__.py``: + + .. literalinclude:: package/tutorial/__init__.py + +#. Enter the following into ``package/tutorial/app.py``: + + .. literalinclude:: package/tutorial/app.py + +#. Run the WSGI application with: + + .. code-block:: bash + + (env27)$ python tutorial/app.py + +#. Open ``http://localhost:6543/`` in your browser. + +Analysis +======== + +Python packages give us an organized unit of project development. +Python projects, via ``setup.py``, gives us special features when +our package is installed (in this case, in local development mode.) + +In this step we have a Python package called ``tutorial``. We use the +same name in each step of the tutorial, to avoid unnecessary re-typing. + +Above this ``tutorial`` directory we have the files that handle the +packaging of this, well, package. At the moment, all we need is a +bare-bones ``ini/setup.py``. + +Everything else is the same about our application. We simply made a +Python package with a ``setup.py`` and installed it in development mode. + +Note that the way we're running the app (``python tutorial/app.py``) is a bit +of an odd duck. We would never do this unless we were writing a tutorial that +tries to capture how this stuff works a step at a time. It's generally a bad +idea to run a Python module inside a package directly as a script. + +.. seealso:: :ref:`Python Packages `, + `setuptools Entry Points `_ diff --git a/docs/quick_tutorial/package/setup.py b/docs/quick_tutorial/package/setup.py new file mode 100644 index 000000000..bcfcfa684 --- /dev/null +++ b/docs/quick_tutorial/package/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, +) \ No newline at end of file diff --git a/docs/quick_tutorial/package/tutorial/__init__.py b/docs/quick_tutorial/package/tutorial/__init__.py new file mode 100644 index 000000000..d310fdde9 --- /dev/null +++ b/docs/quick_tutorial/package/tutorial/__init__.py @@ -0,0 +1 @@ +# package \ No newline at end of file diff --git a/docs/quick_tutorial/package/tutorial/app.py b/docs/quick_tutorial/package/tutorial/app.py new file mode 100644 index 000000000..210075023 --- /dev/null +++ b/docs/quick_tutorial/package/tutorial/app.py @@ -0,0 +1,17 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + print ('Incoming request') + return Response('

Hello World!

') + + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() \ No newline at end of file diff --git a/docs/quick_tutorial/pyramid_setup.rst b/docs/quick_tutorial/pyramid_setup.rst new file mode 100644 index 000000000..5efcc7dc7 --- /dev/null +++ b/docs/quick_tutorial/pyramid_setup.rst @@ -0,0 +1,27 @@ +============= +Pyramid Setup +============= + +Installing Pyramid is easy and normal from a Python packaging +perspective. Again, *make sure* you have your virtual environment first +in your path using ``source bin/activate``. + +.. code-block:: bash + + (env27)$ easy_install pyramid + ....chuggalugga... + (env27ß)$ which pserve + +You now have Pyramid installed. The second command confirms this by +looking for the Pyramid ``pserve`` command that should be on your +``$PATH`` in the ``bin`` of your virtual environment. + +Installing Everything +===================== + +Later parts of the tutorial install more packages. Most likely, +you'd like to go ahead and get much of it now: + +.. code-block:: bash + + (env27)$ easy_install pyramid nose webtest deform sqlalchemy \ No newline at end of file diff --git a/docs/quick_tutorial/python_setup.rst b/docs/quick_tutorial/python_setup.rst new file mode 100644 index 000000000..2d56a0327 --- /dev/null +++ b/docs/quick_tutorial/python_setup.rst @@ -0,0 +1,88 @@ +============ +Python Setup +============ + +First things first: we need our Python environment in ship-shape. +Pyramid encourages standard Python development practices (virtual +environments, packaging tools, logging, etc.) so let's get our working +area in place. + +.. note:: + + This tutorial is aimed at Python 2.7. It also works with + Python 3.3. + +*This step has most likely been performed already on the CCDC computers.* + +Prequisites +=========== + +Modern Python development means two tools to add to the standard +Python installation: packaging and virtual environments. + +Python's tools for installing packages is undergoing rapid change. For +this tutorial, we will install the latest version of +`setuptools `_. This gives us +the ``easy_install`` command-line tool for installing Python packages. +Presuming you have Python on your ``PATH``: + +.. code-block:: bash + + $ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python + +We now have an ``easy_install`` command that we can use to install +``virtualenv``: + +.. code-block:: bash + + $ easy_install virtualenv + +Making a Virtual Environment +============================ + +Developing in isolation helps us ensure what we are learning doesn't +conflict with any packages installed from other work on the machine. +*Virtual environments* let us do just this. + +Presuming you have made a tutorial area at some location (referenced as +``your/tutorial/directory`` below): + +.. code-block:: bash + + $ cd your/tutorial/directory + $ virtualenv env27 + $ source env27/bin/activate + (env27)$ which python2.7 + +Once you do this, your path will be setup to point at the ``bin`` of +your virtual environment. Your prompt will also change, as noted above. + +.. note:: + + This tutorial presumes you are working in a command-line shell + which has performed the ``source env27/bin/activate``. If you + close that shell, or open a new one, you will need to re-perform + that command. + +Discussion +========== + +The modern world of Python packaging eschews ``easy_install`` in favor +of ``pip``, a more-recent and maintained packaging tool. Why doesn't +this tutorial use it? + +- ``pip`` is only gradually getting the ability to install Windows + binaries. ``easy_install`` has been able to do this for years. + +- Until recently, ``pip`` has not been able to use "namespace + packages." As the ``pip`` support for this stabilizes, + we can switch to using ``pip``. + +- You have to use ``easy_install`` to get ``pip`` installed, so why not + just stick with ``easy_install``. + +Python 3.3 has a `built-in story for virtual +environments `_. This +eliminates the requirement for installing ``virtualenv``. Instead, +Python 3.3 provides the ``pyvenv`` command for creating virtual +environments. \ No newline at end of file diff --git a/docs/quick_tutorial/request_response.rst b/docs/quick_tutorial/request_response.rst new file mode 100644 index 000000000..74409711f --- /dev/null +++ b/docs/quick_tutorial/request_response.rst @@ -0,0 +1,101 @@ +======================================= +10: Handling Web Requests and Responses +======================================= + +Web applications handle incoming requests and return outgoing responses. +Pyramid makes working with requests and responses convenient and +reliable. + +Objectives +========== + +- Learn the background on Pyramid's choices for requests and responses + +- Grab data out of the request + +- Change information in the response headers + +Background +========== + +Developing for the web means processing web requests. As this is a +critical part of a web application, web developers need a robust, +mature set of software for web requests and returning web +responses. + +Pyramid has always fit nicely into the existing world of Python web +development (virtual environments, packaging, scaffolding, +first to embrace Python 3, etc.) For request handling, Pyramid turned +to the well-regarded :term:`WebOb` Python library for request and +response handling. In our example +above, Pyramid hands ``hello_world`` a ``request`` that is +:ref:`based on WebOb `. + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + (env27)$ cd ..; cp -r view_classes request_response; cd request_response + (env27)$ python setup.py develop + +#. Simplify the routes in ``request_response/tutorial/__init__.py``: + + .. literalinclude:: request_response/tutorial/__init__.py + +#. We only need one view in ``request_response/tutorial/views.py``: + + .. literalinclude:: request_response/tutorial/views.py + +#. Update the tests in ``request_response/tutorial/tests.py``: + + .. literalinclude:: request_response/tutorial/tests.py + +#. Now run the tests: + + .. code-block:: bash + + (env27)$ nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + (env27)$ pserve development.ini --reload + +#. Open ``http://localhost:6543/`` in your browser. You will be + redirected to ``http://localhost:6543/plain`` + +#. Open ``http://localhost:6543/plain?name=alice`` in your browser. + +Analysis +======== + +In this view class we have two routes and two views, with the first +leading to the second by an HTTP redirect. Pyramid can +:ref:`generate redirects ` by returning a +special object from a view or raising a special exception. + +In this Pyramid view, we get the URL being visited from ``request.url``. +Also, if you visited ``http://localhost:6543/plain?name=alice``, +the name is included in the body of the response:: + + URL http://localhost:6543/plain?name=alice with name: alice + +Finally, we set the response's content type and body, then return the +Response. + +We updated the unit and functional tests to prove that our code +does the redirection, but also handles sending and not sending +``/plain?name``. + +Extra Credit +============ + +#. Could we also ``raise HTTPFound(location='/plain')`` instead of + returning it? If so, what's the difference? + +.. seealso:: :ref:`pyramid:webob_chapter`, + :ref:`generate redirects ` diff --git a/docs/quick_tutorial/request_response/development.ini b/docs/quick_tutorial/request_response/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/request_response/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/request_response/setup.py b/docs/quick_tutorial/request_response/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/request_response/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) \ No newline at end of file diff --git a/docs/quick_tutorial/request_response/tutorial/__init__.py b/docs/quick_tutorial/request_response/tutorial/__init__.py new file mode 100644 index 000000000..77a172888 --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('plain', '/plain') + config.scan('.views') + return config.make_wsgi_app() \ No newline at end of file diff --git a/docs/quick_tutorial/request_response/tutorial/home.pt b/docs/quick_tutorial/request_response/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/home.pt @@ -0,0 +1,9 @@ + + + + Quick Tour: ${name} + + +

Hi ${name}

+ + \ No newline at end of file diff --git a/docs/quick_tutorial/request_response/tutorial/tests.py b/docs/quick_tutorial/request_response/tutorial/tests.py new file mode 100644 index 000000000..87c853375 --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/tests.py @@ -0,0 +1,54 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual(response.status, '302 Found') + + def test_plain_without_name(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.plain() + self.assertIn('No Name Provided', response.body) + + def test_plain_with_name(self): + from .views import TutorialViews + + request = testing.DummyRequest() + request.GET['name'] = 'Jane Doe' + inst = TutorialViews(request) + response = inst.plain() + self.assertIn('Jane Doe', response.body) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_plain_without_name(self): + res = self.testapp.get('/plain', status=200) + self.assertIn(b'No Name Provided', res.body) + + def test_plain_with_name(self): + res = self.testapp.get('/plain?name=Jane%20Doe', status=200) + self.assertIn(b'Jane Doe', res.body) diff --git a/docs/quick_tutorial/request_response/tutorial/views.py b/docs/quick_tutorial/request_response/tutorial/views.py new file mode 100644 index 000000000..8c7ff5f37 --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/views.py @@ -0,0 +1,22 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.response import Response +from pyramid.view import view_config + + +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return HTTPFound(location='/plain') + + @view_config(route_name='plain') + def plain(self): + name = self.request.params.get('name', 'No Name Provided') + + body = 'URL %s with name: %s' % (self.request.url, name) + return Response( + content_type='text/plain', + body=body + ) diff --git a/docs/quick_tutorial/rest_ajax.rst b/docs/quick_tutorial/rest_ajax.rst new file mode 100644 index 000000000..69a940818 --- /dev/null +++ b/docs/quick_tutorial/rest_ajax.rst @@ -0,0 +1,62 @@ +================== +29: REST with Ajax +================== + +Use Ajax operations to talk to a REST interface. + +Objectives +========== + +- Populate a list with JSON data + +- Update contents with client-side forms that post to REST operations + +Steps +===== + +#. We are going to use the previous step as our starting point: + + .. code-block:: bash + + (env27)$ cd ..; cp -r rest_ajax_layout rest_ajax; cd rest_ajax + (env27)$ python setup.py develop + +#. Let's first add a Javascript file that implements our browser-side + logic and talks to the REST service: + +#. Introduce ``pyramid_jinja2`` dependency in + ``rest_ajax/tutorial/static/site.js``: + + .. literalinclude:: rest_ajax/tutorial/static/site.js + :language: js + :linenos: + +#. Add a ``