summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--CHANGES.txt423
-rw-r--r--CONTRIBUTORS.txt6
-rw-r--r--HISTORY.txt415
-rw-r--r--RELEASING.txt26
-rw-r--r--TODO.txt4
-rw-r--r--contributing.md7
-rw-r--r--docs/_static/pyramid_request_processing.graffle893
-rw-r--r--docs/_static/pyramid_request_processing.pngbin122854 -> 130688 bytes
-rw-r--r--docs/_static/pyramid_request_processing.svg2
-rw-r--r--docs/api/config.rst1
-rw-r--r--docs/api/events.rst2
-rw-r--r--docs/api/exceptions.rst2
-rw-r--r--docs/api/interfaces.rst9
-rw-r--r--docs/api/paster.rst2
-rw-r--r--docs/api/request.rst5
-rw-r--r--docs/api/session.rst2
-rw-r--r--docs/api/viewderivers.rst17
-rw-r--r--docs/conf.py10
-rw-r--r--docs/conventions.rst40
-rw-r--r--docs/designdefense.rst196
-rw-r--r--docs/glossary.rst89
-rw-r--r--docs/index.rst1
-rw-r--r--docs/narr/MyProject/README.txt11
-rw-r--r--docs/narr/MyProject/development.ini4
-rw-r--r--docs/narr/MyProject/myproject/static/theme.min.css1
-rw-r--r--docs/narr/MyProject/myproject/templates/mytemplate.pt6
-rw-r--r--docs/narr/MyProject/myproject/tests.py2
-rw-r--r--docs/narr/MyProject/production.ini4
-rw-r--r--docs/narr/MyProject/setup.py21
-rw-r--r--docs/narr/commandline.rst116
-rw-r--r--docs/narr/extconfig.rst1
-rw-r--r--docs/narr/extending.rst4
-rw-r--r--docs/narr/hooks.rst153
-rw-r--r--docs/narr/i18n.rst60
-rw-r--r--docs/narr/install.rst381
-rw-r--r--docs/narr/introduction.rst17
-rw-r--r--docs/narr/project.rst178
-rw-r--r--docs/narr/router.rst25
-rw-r--r--docs/narr/sessions.rst82
-rw-r--r--docs/narr/subrequest.rst50
-rw-r--r--docs/narr/testing.rst47
-rw-r--r--docs/narr/upgrading.rst2
-rw-r--r--docs/narr/viewconfig.rst32
-rw-r--r--docs/quick_tour.rst185
-rw-r--r--docs/quick_tour/sqla_demo/README.txt2
-rw-r--r--docs/quick_tour/sqla_demo/development.ini2
-rw-r--r--docs/quick_tour/sqla_demo/setup.py2
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/__init__.py12
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/models.py27
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py7
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/models/meta.py49
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py19
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py21
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css8
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/pylons.css372
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css1
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/static/transparent.gifbin49 -> 0 bytes
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt)18
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja28
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt67
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/tests.py80
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py0
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/views/default.py (renamed from docs/quick_tour/sqla_demo/sqla_demo/views.py)17
-rw-r--r--docs/quick_tutorial/authentication.rst93
-rw-r--r--docs/quick_tutorial/authorization.rst90
-rw-r--r--docs/quick_tutorial/conf.py281
-rw-r--r--docs/quick_tutorial/databases.rst188
-rw-r--r--docs/quick_tutorial/databases/sqltutorial.sqlitebin12288 -> 0 bytes
-rw-r--r--docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt6
-rw-r--r--docs/quick_tutorial/debugtoolbar.rst60
-rw-r--r--docs/quick_tutorial/forms.rst129
-rw-r--r--docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt6
-rw-r--r--docs/quick_tutorial/functional_testing.rst56
-rw-r--r--docs/quick_tutorial/hello_world.rst81
-rw-r--r--docs/quick_tutorial/index.rst10
-rw-r--r--docs/quick_tutorial/ini.rst94
-rw-r--r--docs/quick_tutorial/jinja2.rst59
-rw-r--r--docs/quick_tutorial/json.rst89
-rw-r--r--docs/quick_tutorial/logging.rst54
-rw-r--r--docs/quick_tutorial/more_view_classes.rst133
-rw-r--r--docs/quick_tutorial/package.rst78
-rw-r--r--docs/quick_tutorial/request_response.rst73
-rw-r--r--docs/quick_tutorial/requirements.rst213
-rw-r--r--docs/quick_tutorial/routing.rst71
-rw-r--r--docs/quick_tutorial/scaffolds.rst55
-rw-r--r--docs/quick_tutorial/sessions.rst72
-rw-r--r--docs/quick_tutorial/static_assets.rst47
-rw-r--r--docs/quick_tutorial/templating.rst87
-rw-r--r--docs/quick_tutorial/tutorial_approach.rst66
-rw-r--r--docs/quick_tutorial/unit_testing.rst116
-rw-r--r--docs/quick_tutorial/view_classes.rst80
-rw-r--r--docs/quick_tutorial/views.rst87
-rw-r--r--docs/tutorials/modwsgi/index.rst29
-rw-r--r--docs/tutorials/wiki/authorization.rst44
-rw-r--r--docs/tutorials/wiki/background.rst2
-rw-r--r--docs/tutorials/wiki/basiclayout.rst33
-rw-r--r--docs/tutorials/wiki/definingmodels.rst9
-rw-r--r--docs/tutorials/wiki/definingviews.rst39
-rw-r--r--docs/tutorials/wiki/design.rst2
-rw-r--r--docs/tutorials/wiki/distributing.rst19
-rw-r--r--docs/tutorials/wiki/index.rst1
-rw-r--r--docs/tutorials/wiki/installation.rst317
-rw-r--r--docs/tutorials/wiki/src/authorization/CHANGES.txt3
-rw-r--r--docs/tutorials/wiki/src/authorization/README.txt8
-rw-r--r--docs/tutorials/wiki/src/authorization/development.ini8
-rw-r--r--docs/tutorials/wiki/src/authorization/production.ini6
-rw-r--r--docs/tutorials/wiki/src/authorization/setup.py21
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/models.py2
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt5
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/tests.py124
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/views.py33
-rw-r--r--docs/tutorials/wiki/src/basiclayout/README.txt8
-rw-r--r--docs/tutorials/wiki/src/basiclayout/development.ini8
-rw-r--r--docs/tutorials/wiki/src/basiclayout/production.ini6
-rw-r--r--docs/tutorials/wiki/src/basiclayout/setup.py21
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/models.py2
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt5
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/tests.py1
-rw-r--r--docs/tutorials/wiki/src/installation/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki/src/installation/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki/src/installation/README.txt12
-rw-r--r--docs/tutorials/wiki/src/installation/development.ini65
-rw-r--r--docs/tutorials/wiki/src/installation/production.ini60
-rw-r--r--docs/tutorials/wiki/src/installation/setup.py53
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/__init__.py18
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/models.py14
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/static/pyramid.pngbin0 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/templates/mytemplate.pt (renamed from docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt)9
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/tests.py17
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/views.py7
-rw-r--r--docs/tutorials/wiki/src/models/CHANGES.txt2
-rw-r--r--docs/tutorials/wiki/src/models/README.txt8
-rw-r--r--docs/tutorials/wiki/src/models/development.ini8
-rw-r--r--docs/tutorials/wiki/src/models/production.ini6
-rw-r--r--docs/tutorials/wiki/src/models/setup.py21
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/models.py2
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt5
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/tests.py44
-rw-r--r--docs/tutorials/wiki/src/tests/CHANGES.txt3
-rw-r--r--docs/tutorials/wiki/src/tests/README.txt8
-rw-r--r--docs/tutorials/wiki/src/tests/development.ini8
-rw-r--r--docs/tutorials/wiki/src/tests/production.ini6
-rw-r--r--docs/tutorials/wiki/src/tests/setup.py22
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/__init__.py2
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/models.py2
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt5
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/tests.py4
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/views.py33
-rw-r--r--docs/tutorials/wiki/src/views/CHANGES.txt5
-rw-r--r--docs/tutorials/wiki/src/views/README.txt8
-rw-r--r--docs/tutorials/wiki/src/views/development.ini8
-rw-r--r--docs/tutorials/wiki/src/views/production.ini6
-rw-r--r--docs/tutorials/wiki/src/views/setup.py21
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/models.py2
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt5
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/templates/view.pt2
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/tests.py127
-rw-r--r--docs/tutorials/wiki/tests.rst46
-rw-r--r--docs/tutorials/wiki2/authentication.rst312
-rw-r--r--docs/tutorials/wiki2/authorization.rst501
-rw-r--r--docs/tutorials/wiki2/background.rst6
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst398
-rw-r--r--docs/tutorials/wiki2/definingmodels.rst288
-rw-r--r--docs/tutorials/wiki2/definingviews.rst494
-rw-r--r--docs/tutorials/wiki2/design.rst256
-rw-r--r--docs/tutorials/wiki2/distributing.rst14
-rw-r--r--docs/tutorials/wiki2/index.rst13
-rw-r--r--docs/tutorials/wiki2/installation.rst464
-rw-r--r--docs/tutorials/wiki2/src/authentication/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki2/src/authentication/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/authentication/README.txt14
-rw-r--r--docs/tutorials/wiki2/src/authentication/development.ini73
-rw-r--r--docs/tutorials/wiki2/src/authentication/production.ini62
-rw-r--r--docs/tutorials/wiki2/src/authentication/setup.py57
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/__init__.py13
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/routes.py8
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py1
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py57
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/security.py27
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.pngbin0 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/views/tutorial/templates/view.pt)37
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/tests.py65
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/default.py79
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/authorization/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/authorization/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/authorization/development.ini10
-rw-r--r--docs/tutorials/wiki2/src/authorization/production.ini18
-rw-r--r--docs/tutorials/wiki2/src/authorization/setup.py25
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/__init__.py34
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models.py37
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/routes.py56
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py44
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/security.py45
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt72
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt)40
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt74
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/tests.py175
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views.py124
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/default.py64
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/development.ini8
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/production.ini8
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/setup.py22
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py15
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models.py27
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py73
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py3
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py30
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt)18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja28
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py66
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py (renamed from docs/tutorials/wiki2/src/basiclayout/tutorial/views.py)18
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/installation/CHANGES.txt4
-rw-r--r--docs/tutorials/wiki2/src/installation/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/installation/README.txt14
-rw-r--r--docs/tutorials/wiki2/src/installation/development.ini71
-rw-r--r--docs/tutorials/wiki2/src/installation/production.ini60
-rw-r--r--docs/tutorials/wiki2/src/installation/setup.py55
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/__init__.py12
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py73
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py18
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/routes.py3
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py1
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py45
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.pngbin0 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt)18
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja28
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/tests.py65
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/views/default.py (renamed from docs/tutorials/wiki2/src/models/tutorial/views.py)18
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/models/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/models/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/models/development.ini8
-rw-r--r--docs/tutorials/wiki2/src/models/production.ini16
-rw-r--r--docs/tutorials/wiki2/src/models/setup.py23
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/__init__.py15
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models.py25
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/routes.py3
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py44
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja266
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja28
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/tests.py66
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/default.py33
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/tests/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/tests/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/tests/development.ini10
-rw-r--r--docs/tutorials/wiki2/src/tests/production.ini18
-rw-r--r--docs/tutorials/wiki2/src/tests/setup.py26
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/__init__.py34
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models.py37
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/routes.py56
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py44
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/security.py45
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt74
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt)37
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt54
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests.py235
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py122
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py168
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views.py123
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/default.py64
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/views/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/views/README.txt2
-rw-r--r--docs/tutorials/wiki2/src/views/development.ini8
-rw-r--r--docs/tutorials/wiki2/src/views/production.ini16
-rw-r--r--docs/tutorials/wiki2/src/views/setup.py25
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/__init__.py18
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models.py25
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/__init__.py74
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/meta.py16
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/page.py20
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/user.py29
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/routes.py6
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py44
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/theme.min.css1
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja220
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 (renamed from docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt)33
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt66
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/tests.py175
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views.py72
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/default.py73
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/tests.rst109
-rw-r--r--docs/whatsnew-1.6.rst2
-rw-r--r--docs/whatsnew-1.7.rst172
-rw-r--r--pyramid/authentication.py49
-rw-r--r--pyramid/config/__init__.py4
-rw-r--r--pyramid/config/predicates.py7
-rw-r--r--pyramid/config/settings.py10
-rw-r--r--pyramid/config/tweens.py7
-rw-r--r--pyramid/config/util.py11
-rw-r--r--pyramid/config/views.py878
-rw-r--r--pyramid/events.py24
-rw-r--r--pyramid/exceptions.py15
-rw-r--r--pyramid/httpexceptions.py50
-rw-r--r--pyramid/interfaces.py41
-rw-r--r--pyramid/paster.py18
-rw-r--r--pyramid/renderers.py23
-rw-r--r--pyramid/request.py2
-rw-r--r--pyramid/router.py15
-rw-r--r--pyramid/scaffolds/alchemy/+package+/__init__.py15
-rw-r--r--pyramid/scaffolds/alchemy/+package+/models.py27
-rw-r--r--pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl73
-rw-r--r--pyramid/scaffolds/alchemy/+package+/models/meta.py16
-rw-r--r--pyramid/scaffolds/alchemy/+package+/models/mymodel.py18
-rw-r--r--pyramid/scaffolds/alchemy/+package+/routes.py3
-rw-r--r--pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py21
-rw-r--r--pyramid/scaffolds/alchemy/+package+/static/theme.min.css1
-rw-r--r--pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl8
-rw-r--r--pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl (renamed from pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl)15
-rw-r--r--pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl8
-rw-r--r--pyramid/scaffolds/alchemy/+package+/tests.py_tmpl78
-rw-r--r--pyramid/scaffolds/alchemy/+package+/views/__init__.py0
-rw-r--r--pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl (renamed from pyramid/scaffolds/alchemy/+package+/views.py_tmpl)15
-rw-r--r--pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl7
-rw-r--r--pyramid/scaffolds/alchemy/MANIFEST.in_tmpl2
-rw-r--r--pyramid/scaffolds/alchemy/README.txt_tmpl2
-rw-r--r--pyramid/scaffolds/alchemy/production.ini_tmpl2
-rw-r--r--pyramid/scaffolds/alchemy/setup.py_tmpl22
-rw-r--r--pyramid/scaffolds/starter/+package+/static/theme.min.css1
-rw-r--r--pyramid/scaffolds/starter/+package+/tests.py_tmpl12
-rw-r--r--pyramid/scaffolds/starter/README.txt_tmpl11
-rw-r--r--pyramid/scaffolds/starter/setup.py_tmpl21
-rw-r--r--pyramid/scaffolds/zodb/+package+/static/theme.css8
-rw-r--r--pyramid/scaffolds/zodb/+package+/static/theme.min.css1
-rw-r--r--pyramid/scaffolds/zodb/README.txt_tmpl11
-rw-r--r--pyramid/scaffolds/zodb/setup.py_tmpl21
-rw-r--r--pyramid/scripts/pserve.py2
-rw-r--r--pyramid/session.py139
-rw-r--r--pyramid/settings.py7
-rw-r--r--pyramid/static.py4
-rw-r--r--pyramid/testing.py3
-rw-r--r--pyramid/tests/test_config/test_predicates.py16
-rw-r--r--pyramid/tests/test_config/test_views.py1212
-rw-r--r--pyramid/tests/test_events.py35
-rw-r--r--pyramid/tests/test_httpexceptions.py121
-rw-r--r--pyramid/tests/test_paster.py26
-rw-r--r--pyramid/tests/test_renderers.py42
-rw-r--r--pyramid/tests/test_router.py4
-rw-r--r--pyramid/tests/test_session.py102
-rw-r--r--pyramid/tests/test_util.py78
-rw-r--r--pyramid/tests/test_view.py132
-rw-r--r--pyramid/tests/test_viewderivers.py1658
-rw-r--r--pyramid/util.py40
-rw-r--r--pyramid/view.py79
-rw-r--r--pyramid/viewderivers.py508
-rw-r--r--setup.py52
-rw-r--r--tox.ini11
422 files changed, 13947 insertions, 8889 deletions
diff --git a/.travis.yml b/.travis.yml
index 39f0ca435..e45f3df7d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,8 +4,6 @@ sudo: false
matrix:
include:
- - python: 2.6
- env: TOXENV=py26
- python: 2.7
env: TOXENV=py27
- python: 3.3
diff --git a/CHANGES.txt b/CHANGES.txt
index ffa5f51e0..d316594bc 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,332 +1,139 @@
unreleased
==========
-- Dropped Python 3.2 support.
- See https://github.com/Pylons/pyramid/pull/2256
-
-- Fix ``pserve --browser`` to use the ``--server-name`` instead of the
- app name when selecting a section to use. This was only working for people
- who had server and app sections with the same name, for example
- ``[app:main]`` and ``[server:main]``.
- See https://github.com/Pylons/pyramid/pull/2292
-
-1.6 (2015-04-14)
-================
-
Backward Incompatibilities
--------------------------
-- IPython and BPython support have been removed from pshell in the core.
- To continue using them on Pyramid 1.6+ you must install the binding
- packages explicitly::
+- Following the Pyramid deprecation period (1.4 -> 1.6),
+ AuthTktAuthenticationPolicy's default hashing algorithm is changing from md5
+ to sha512. If you are using the authentication policy and need to continue
+ using md5, please explicitly set hashalg to 'md5'.
- $ pip install pyramid_ipython
+ This change does mean that any existing auth tickets (and associated cookies)
+ will no longer be valid, and users will no longer be logged in, and have to
+ login to their accounts again.
- or
+ See https://github.com/Pylons/pyramid/pull/2496
- $ pip install pyramid_bpython
+- The ``check_csrf_token`` function no longer validates a csrf token in the
+ query string of a request. Only headers and request bodies are supported.
+ See https://github.com/Pylons/pyramid/pull/2500
Features
--------
-- pcreate will now ask for confirmation if invoked with
- an argument for a project name that already exists or
- is importable in the current environment.
- See https://github.com/Pylons/pyramid/issues/1357 and
- https://github.com/Pylons/pyramid/pull/1837
-
-- Make it possible to subclass ``pyramid.request.Request`` and also use
- ``pyramid.request.Request.add_request.method``. See
- https://github.com/Pylons/pyramid/issues/1529
-
-- The ``pyramid.config.Configurator`` has grown the ability to allow
- actions to call other actions during a commit-cycle. This enables much more
- logic to be placed into actions, such as the ability to invoke other actions
- or group them for improved conflict detection. We have also exposed and
- documented the config phases that Pyramid uses in order to further assist
- in building conforming addons.
- See https://github.com/Pylons/pyramid/pull/1513
-
-- Add ``pyramid.request.apply_request_extensions`` function which can be
- used in testing to apply any request extensions configured via
- ``config.add_request_method``. Previously it was only possible to test
- the extensions by going through Pyramid's router.
- See https://github.com/Pylons/pyramid/pull/1581
-
-- pcreate when run without a scaffold argument will now print information on
- the missing flag, as well as a list of available scaffolds.
- See https://github.com/Pylons/pyramid/pull/1566 and
- https://github.com/Pylons/pyramid/issues/1297
-
-- Added support / testing for 'pypy3' under Tox and Travis.
- See https://github.com/Pylons/pyramid/pull/1469
-
-- Automate code coverage metrics across py2 and py3 instead of just py2.
- See https://github.com/Pylons/pyramid/pull/1471
-
-- Cache busting for static resources has been added and is available via a new
- ``pyramid.config.Configurator.add_cache_buster`` API. Core APIs are shipped
- for both cache busting via query strings and via asset manifests for
- integrating into custom asset pipelines.
- See https://github.com/Pylons/pyramid/pull/1380 and
- https://github.com/Pylons/pyramid/pull/1583 and
- https://github.com/Pylons/pyramid/pull/2171
-
-- Add ``pyramid.config.Configurator.root_package`` attribute and init
- parameter to assist with includeable packages that wish to resolve
- resources relative to the package in which the ``Configurator`` was created.
- This is especially useful for addons that need to load asset specs from
- settings, in which case it is may be natural for a developer to define
- imports or assets relative to the top-level package.
- See https://github.com/Pylons/pyramid/pull/1337
-
-- Added line numbers to the log formatters in the scaffolds to assist with
- debugging. See https://github.com/Pylons/pyramid/pull/1326
-
-- Add new HTTP exception objects for status codes
- ``428 Precondition Required``, ``429 Too Many Requests`` and
- ``431 Request Header Fields Too Large`` in ``pyramid.httpexceptions``.
- See https://github.com/Pylons/pyramid/pull/1372/files
-
-- The ``pshell`` script will now load a ``PYTHONSTARTUP`` file if one is
- defined in the environment prior to launching the interpreter.
- See https://github.com/Pylons/pyramid/pull/1448
-
-- Make it simple to define notfound and forbidden views that wish to use
- the default exception-response view but with altered predicates and other
- configuration options. The ``view`` argument is now optional in
- ``config.add_notfound_view`` and ``config.add_forbidden_view``..
- See https://github.com/Pylons/pyramid/issues/494
-
-- Greatly improve the readability of the ``pcreate`` shell script output.
- See https://github.com/Pylons/pyramid/pull/1453
-
-- Improve robustness to timing attacks in the ``AuthTktCookieHelper`` and
- the ``SignedCookieSessionFactory`` classes by using the stdlib's
- ``hmac.compare_digest`` if it is available (such as Python 2.7.7+ and 3.3+).
- See https://github.com/Pylons/pyramid/pull/1457
-
-- Assets can now be overidden by an absolute path on the filesystem when using
- the ``config.override_asset`` API. This makes it possible to fully support
- serving up static content from a mutable directory while still being able
- to use the ``request.static_url`` API and ``config.add_static_view``.
- Previously it was not possible to use ``config.add_static_view`` with an
- absolute path **and** generate urls to the content. This change replaces
- the call, ``config.add_static_view('/abs/path', 'static')``, with
- ``config.add_static_view('myapp:static', 'static')`` and
- ``config.override_asset(to_override='myapp:static/',
- override_with='/abs/path/')``. The ``myapp:static`` asset spec is completely
- made up and does not need to exist - it is used for generating urls
- via ``request.static_url('myapp:static/foo.png')``.
- See https://github.com/Pylons/pyramid/issues/1252
-
-- Added ``pyramid.config.Configurator.set_response_factory`` and the
- ``response_factory`` keyword argument to the ``Configurator`` for defining
- a factory that will return a custom ``Response`` class.
- See https://github.com/Pylons/pyramid/pull/1499
-
-- Allow an iterator to be returned from a renderer. Previously it was only
- possible to return bytes or unicode.
- See https://github.com/Pylons/pyramid/pull/1417
-
-- ``pserve`` can now take a ``-b`` or ``--browser`` option to open the server
- URL in a web browser. See https://github.com/Pylons/pyramid/pull/1533
-
-- Overall improvments for the ``proutes`` command. Added ``--format`` and
- ``--glob`` arguments to the command, introduced the ``method``
- column for displaying available request methods, and improved the ``view``
- output by showing the module instead of just ``__repr__``.
- See https://github.com/Pylons/pyramid/pull/1488
-
-- Support keyword-only arguments and function annotations in views in
- Python 3. See https://github.com/Pylons/pyramid/pull/1556
-
-- ``request.response`` will no longer be mutated when using the
- ``pyramid.renderers.render_to_response()`` API. It is now necessary to
- pass in a ``response=`` argument to ``render_to_response`` if you wish to
- supply the renderer with a custom response object for it to use. If you
- do not pass one then a response object will be created using the
- application's ``IResponseFactory``. Almost all renderers
- mutate the ``request.response`` response object (for example, the JSON
- renderer sets ``request.response.content_type`` to ``application/json``).
- However, when invoking ``render_to_response`` it is not expected that the
- response object being returned would be the same one used later in the
- request. The response object returned from ``render_to_response`` is now
- explicitly different from ``request.response``. This does not change the
- API of a renderer. See https://github.com/Pylons/pyramid/pull/1563
-
-- The ``append_slash`` argument of ```Configurator().add_notfound_view()`` will
- now accept anything that implements the ``IResponse`` interface and will use
- that as the response class instead of the default ``HTTPFound``. See
- https://github.com/Pylons/pyramid/pull/1610
-
-- Additional shells for ``pshell`` can now be registered as entrypoints. See
- https://github.com/Pylons/pyramid/pull/1891 and
- https://github.com/Pylons/pyramid/pull/2012
-
-- The variables injected into ``pshell`` are now displayed with their
- docstrings instead of the default ``str(obj)`` when possible.
- See https://github.com/Pylons/pyramid/pull/1929
-
-- ``pserve --reload`` will no longer crash on syntax errors!!!
- See https://github.com/Pylons/pyramid/pull/2044
+- Added a new setting, ``pyramid.require_default_csrf`` which may be used
+ to turn on CSRF checks globally for every POST request in the application.
+ This should be considered a good default for websites built on Pyramid.
+ It is possible to opt-out of CSRF checks on a per-view basis by setting
+ ``require_csrf=False`` on those views.
+ See https://github.com/Pylons/pyramid/pull/2413
+
+- Added a ``require_csrf`` view option which will enforce CSRF checks on any
+ request with an unsafe method as defined by RFC2616. If the CSRF check fails
+ a ``BadCSRFToken`` exception will be raised and may be caught by exception
+ views (the default response is a ``400 Bad Request``). This option should be
+ used in place of the deprecated ``check_csrf`` view predicate which would
+ normally result in unexpected ``404 Not Found`` response to the client
+ instead of a catchable exception. See
+ https://github.com/Pylons/pyramid/pull/2413 and
+ https://github.com/Pylons/pyramid/pull/2500
+
+- Added an additional CSRF validation that checks the origin/referrer of a
+ request and makes sure it matches the current ``request.domain``. This
+ particular check is only active when accessing a site over HTTPS as otherwise
+ browsers don't always send the required information. If this additional CSRF
+ validation fails a ``BadCSRFOrigin`` exception will be raised and may be
+ caught by exception views (the default response is ``400 Bad Request``).
+ Additional allowed origins may be configured by setting
+ ``pyramid.csrf_trusted_origins`` to a list of domain names (with ports if on
+ a non standard port) to allow. Subdomains are not allowed unless the domain
+ name has been prefixed with a ``.``. See
+ https://github.com/Pylons/pyramid/pull/2501
+
+- Added a new ``pyramid.session.check_csrf_origin`` API for validating the
+ origin or referrer headers against the request's domain.
+ See https://github.com/Pylons/pyramid/pull/2501
+
+- Pyramid HTTPExceptions will now take into account the best match for the
+ clients Accept header, and depending on what is requested will return
+ text/html, application/json or text/plain. The default for */* is still
+ text/html, but if application/json is explicitly mentioned it will now
+ receive a valid JSON response. See
+ https://github.com/Pylons/pyramid/pull/2489
+
+- A new event and interface (BeforeTraversal) has been introduced that will
+ notify listeners before traversal starts in the router. See
+ https://github.com/Pylons/pyramid/pull/2469 and
+ https://github.com/Pylons/pyramid/pull/1876
+
+- Add a new "view deriver" concept to Pyramid to allow framework authors to
+ inject elements into the standard Pyramid view pipeline and affect all
+ views in an application. This is similar to a decorator except that it
+ has access to options passed to ``config.add_view`` and can affect other
+ stages of the pipeline such as the raw response from a view or prior to
+ security checks. See https://github.com/Pylons/pyramid/pull/2021
+
+- Allow a leading ``=`` on the key of the request param predicate.
+ For example, '=abc=1' is equivalent down to
+ ``request.params['=abc'] == '1'``.
+ See https://github.com/Pylons/pyramid/pull/1370
+
+- A new ``request.invoke_exception_view(...)`` method which can be used to
+ invoke an exception view and get back a response. This is useful for
+ rendering an exception view outside of the context of the excview tween
+ where you may need more control over the request.
+ See https://github.com/Pylons/pyramid/pull/2393
+
+- Allow using variable substitutions like ``%(LOGGING_LOGGER_ROOT_LEVEL)s``
+ for logging sections of the .ini file and populate these variables from
+ the ``pserve`` command line -- e.g.:
+ ``pserve development.ini LOGGING_LOGGER_ROOT_LEVEL=DEBUG``
+ See https://github.com/Pylons/pyramid/pull/2399
+
+Documentation Changes
+---------------------
+
+- A complete overhaul of the docs:
+
+ - Use pip instead of easy_install.
+ - Become opinionated by preferring Python 3.4 or greater to simplify
+ installation of Python and its required packaging tools.
+ - Use venv for the tool, and virtual environment for the thing created,
+ instead of virtualenv.
+ - Use py.test and pytest-cov instead of nose and coverage.
+ - Further updates to the scaffolds as well as tutorials and their src files.
+
+ See https://github.com/Pylons/pyramid/pull/2468
+
+- A complete overhaul of the ``alchemy`` scaffold as well as the
+ Wiki2 SQLAlchemy + URLDispatch tutorial to introduce more modern features
+ into the usage of SQLAlchemy with Pyramid and provide a better starting
+ point for new projects.
+ See https://github.com/Pylons/pyramid/pull/2024
Bug Fixes
---------
-- Work around an issue where ``pserve --reload`` would leave terminal echo
- disabled if it reloaded during a pdb session.
- See https://github.com/Pylons/pyramid/pull/1577,
- https://github.com/Pylons/pyramid/pull/1592
-
-- ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise
- ``ValueError`` when accidentally passed ``None``.
- See https://github.com/Pylons/pyramid/pull/1320
-
-- Fix an issue whereby predicates would be resolved as maybe_dotted in the
- introspectable but not when passed for registration. This would mean that
- ``add_route_predicate`` for example can not take a string and turn it into
- the actual callable function.
- See https://github.com/Pylons/pyramid/pull/1306
-
-- Fix ``pyramid.testing.setUp`` to return a ``Configurator`` with a proper
- package. Previously it was not possible to do package-relative includes
- using the returned ``Configurator`` during testing. There is now a
- ``package`` argument that can override this behavior as well.
- See https://github.com/Pylons/pyramid/pull/1322
-
-- Fix an issue where a ``pyramid.response.FileResponse`` may apply a charset
- where it does not belong. See https://github.com/Pylons/pyramid/pull/1251
-
-- Work around a bug introduced in Python 2.7.7 on Windows where
- ``mimetypes.guess_type`` returns Unicode rather than str for the content
- type, unlike any previous version of Python. See
- https://github.com/Pylons/pyramid/issues/1360 for more information.
-
-- ``pcreate`` now normalizes the package name by converting hyphens to
- underscores. See https://github.com/Pylons/pyramid/pull/1376
-
-- Fix an issue with the final response/finished callback being unable to
- add another callback to the list. See
- https://github.com/Pylons/pyramid/pull/1373
-
-- Fix a failing unittest caused by differing mimetypes across various OSs.
- See https://github.com/Pylons/pyramid/issues/1405
-
-- Fix route generation for static view asset specifications having no path.
- See https://github.com/Pylons/pyramid/pull/1377
-
-- Allow the ``pyramid.renderers.JSONP`` renderer to work even if there is no
- valid request object. In this case it will not wrap the object in a
- callback and thus behave just like the ``pyramid.renderers.JSON`` renderer.
- See https://github.com/Pylons/pyramid/pull/1561
-
-- Prevent "parameters to load are deprecated" ``DeprecationWarning``
- from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541
-
-- Avoiding sharing the ``IRenderer`` objects across threads when attached to
- a view using the `renderer=` argument. These renderers were instantiated
- at time of first render and shared between requests, causing potentially
- subtle effects like `pyramid.reload_templates = true` failing to work
- in `pyramid_mako`. See https://github.com/Pylons/pyramid/pull/1575
- and https://github.com/Pylons/pyramid/issues/1268
-
-- Avoiding timing attacks against CSRF tokens.
- See https://github.com/Pylons/pyramid/pull/1574
-
-- ``request.finished_callbacks`` and ``request.response_callbacks`` now
- default to an iterable instead of ``None``. It may be checked for a length
- of 0. This was the behavior in 1.5.
-
-- ``pyramid.httpexceptions.HTTPException`` now defaults to
- ``520 Unknown Error`` instead of ``None None`` to conform with changes in
- WebOb 1.5.
- See https://github.com/Pylons/pyramid/pull/1865
-
-- ``pshell`` will now preserve the capitalization of variables in the
- ``[pshell]`` section of the INI file. This makes exposing classes to the
- shell a little more straightfoward.
- See https://github.com/Pylons/pyramid/pull/1883
-
-- Fix an issue when user passes unparsed strings to ``pyramid.session.CookieSession``
- and ``pyramid.authentication.AuthTktCookieHelper`` for time related parameters
- ``timeout``, ``reissue_time``, ``max_age`` that expect an integer value.
- See https://github.com/Pylons/pyramid/pull/2050
-
-- Fixed usage of ``pserve --monitor-restart --daemon`` which would fail in
- horrible ways. See https://github.com/Pylons/pyramid/pull/2118
-
-- Explicitly prevent ``pserve --reload --daemon`` from being used. It's never
- been supported but would work and fail in weird ways.
- See https://github.com/Pylons/pyramid/pull/2119
-
-- Fix an issue on Windows when running ``pserve --reload`` in which the
- process failed to fork because it could not find the pserve script to
- run. See https://github.com/Pylons/pyramid/pull/2137
-
-- Ensure that ``IAssetDescriptor.abspath`` always returns an absolute path.
- There were cases depending on the process CWD that a relative path would
- be returned. See https://github.com/Pylons/pyramid/issues/2187
-
+- Fix ``pserve --browser`` to use the ``--server-name`` instead of the
+ app name when selecting a section to use. This was only working for people
+ who had server and app sections with the same name, for example
+ ``[app:main]`` and ``[server:main]``.
+ See https://github.com/Pylons/pyramid/pull/2292
Deprecations
------------
-- The ``pserve`` command's daemonization features have been deprecated as well
- as ``--monitor-restart``. This includes the ``[start,stop,restart,status]``
- subcommands as well as the ``--daemon``, ``--stop-daemon``, ``--pid-file``,
- ``--status``, ``--user`` and ``--group`` flags.
- See https://github.com/Pylons/pyramid/pull/2120
- and https://github.com/Pylons/pyramid/pull/2189
- and https://github.com/Pylons/pyramid/pull/1641
-
- Please use a real process manager in the future instead of relying on the
- ``pserve`` to daemonize itself. Many options exist including your Operating
- System's services such as Systemd or Upstart, as well as Python-based
- solutions like Circus and Supervisor.
-
- See https://github.com/Pylons/pyramid/pull/1641
- and https://github.com/Pylons/pyramid/pull/2120
-
-- Renamed the ``principal`` argument to ``pyramid.security.remember()`` to
- ``userid`` in order to clarify its intended purpose.
- See https://github.com/Pylons/pyramid/pull/1399
-
-Docs
-----
-
-- Moved the documentation for ``accept`` on ``Configurator.add_view`` to no
- longer be part of the predicate list. See
- https://github.com/Pylons/pyramid/issues/1391 for a bug report stating
- ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the
- conclusion that it should not be documented as a predicate.
- See https://github.com/Pylons/pyramid/pull/1487 for this PR
+- The ``check_csrf`` view predicate has been deprecated. Use the
+ new ``require_csrf`` option or the ``pyramid.require_default_csrf`` setting
+ to ensure that the ``BadCSRFToken`` exception is raised.
+ See https://github.com/Pylons/pyramid/pull/2413
-- Removed logging configuration from Quick Tutorial ini files except for
- scaffolding- and logging-related chapters to avoid needing to explain it too
- early.
+- Support for Python 3.3 will be removed in Pyramid 1.8.
+ https://github.com/Pylons/pyramid/issues/2477
-- Clarify a previously-implied detail of the ``ISession.invalidate`` API
- documentation.
-
-- Improve and clarify the documentation on what Pyramid defines as a
- ``principal`` and a ``userid`` in its security APIs.
- See https://github.com/Pylons/pyramid/pull/1399
-
-- Add documentation of command line programs (``p*`` scripts). See
- https://github.com/Pylons/pyramid/pull/2191
-
-Scaffolds
----------
-
-- Update scaffold generating machinery to return the version of pyramid and
- pyramid docs for use in scaffolds. Updated starter, alchemy and zodb
- templates to have links to correctly versioned documentation and reflect
- which pyramid was used to generate the scaffold.
-
-- Removed non-ascii copyright symbol from templates, as this was
- causing the scaffolds to fail for project generation.
-
-- You can now run the scaffolding func tests via ``tox py2-scaffolds`` and
- ``tox py3-scaffolds``.
+- Python 2.6 is no longer supported by Pyramid. See
+ https://github.com/Pylons/pyramid/issues/2368
+- Dropped Python 3.2 support.
+ See https://github.com/Pylons/pyramid/pull/2256
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 7c895ac15..563a995a9 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -247,6 +247,10 @@ Contributors
- Donald Stufft, 2015/03/15
+- Randy Topliffe, 2015/04/14
+
+- Timur Izhbulatov, 2015/04/14
+
- Karen Dalton, 2015/06/01
- Igor Stroh, 2015/06/10
@@ -260,3 +264,5 @@ Contributors
- Sri Sanketh Uppalapati, 2015/12/12
- Marcin Raczyński, 2016/01/26
+
+- Arian Maykon de A. Diógenes, 2016/04/13
diff --git a/HISTORY.txt b/HISTORY.txt
index 68ddb3a90..b7f30ff86 100644
--- a/HISTORY.txt
+++ b/HISTORY.txt
@@ -1,3 +1,418 @@
+1.6 (2016-01-03)
+================
+
+Deprecations
+------------
+
+- Continue removal of ``pserve`` daemon/process management features
+ by deprecating ``--user`` and ``--group`` options.
+ See https://github.com/Pylons/pyramid/pull/2190
+
+1.6b3 (2015-12-17)
+==================
+
+Backward Incompatibilities
+--------------------------
+
+- Remove the ``cachebust`` option from ``config.add_static_view``. See
+ ``config.add_cache_buster`` for the new way to attach cache busters to
+ static assets.
+ See https://github.com/Pylons/pyramid/pull/2186
+
+- Modify the ``pyramid.interfaces.ICacheBuster`` API to be a simple callable
+ instead of an object with ``match`` and ``pregenerate`` methods. Cache
+ busters are now focused solely on generation. Matching has been dropped.
+
+ Note this affects usage of ``pyramid.static.QueryStringCacheBuster`` and
+ ``pyramid.static.ManifestCacheBuster``.
+
+ See https://github.com/Pylons/pyramid/pull/2186
+
+Features
+--------
+
+- Add a new ``config.add_cache_buster`` API for attaching cache busters to
+ static assets. See https://github.com/Pylons/pyramid/pull/2186
+
+Bug Fixes
+---------
+
+- Ensure that ``IAssetDescriptor.abspath`` always returns an absolute path.
+ There were cases depending on the process CWD that a relative path would
+ be returned. See https://github.com/Pylons/pyramid/issues/2188
+
+1.6b2 (2015-10-15)
+==================
+
+Features
+--------
+
+- Allow asset specifications to be supplied to
+ ``pyramid.static.ManifestCacheBuster`` instead of requiring a
+ filesystem path.
+
+1.6b1 (2015-10-15)
+==================
+
+Backward Incompatibilities
+--------------------------
+
+- IPython and BPython support have been removed from pshell in the core.
+ To continue using them on Pyramid 1.6+ you must install the binding
+ packages explicitly::
+
+ $ pip install pyramid_ipython
+
+ or
+
+ $ pip install pyramid_bpython
+
+- Remove default cache busters introduced in 1.6a1 including
+ ``PathSegmentCacheBuster``, ``PathSegmentMd5CacheBuster``, and
+ ``QueryStringMd5CacheBuster``.
+ See https://github.com/Pylons/pyramid/pull/2116
+
+Features
+--------
+
+- Additional shells for ``pshell`` can now be registered as entrypoints. See
+ https://github.com/Pylons/pyramid/pull/1891 and
+ https://github.com/Pylons/pyramid/pull/2012
+
+- The variables injected into ``pshell`` are now displayed with their
+ docstrings instead of the default ``str(obj)`` when possible.
+ See https://github.com/Pylons/pyramid/pull/1929
+
+- Add new ``pyramid.static.ManifestCacheBuster`` for use with external
+ asset pipelines as well as examples of common usages in the narrative.
+ See https://github.com/Pylons/pyramid/pull/2116
+
+- Fix ``pserve --reload`` to not crash on syntax errors!!!
+ See https://github.com/Pylons/pyramid/pull/2125
+
+- Fix an issue when user passes unparsed strings to ``pyramid.session.CookieSession``
+ and ``pyramid.authentication.AuthTktCookieHelper`` for time related parameters
+ ``timeout``, ``reissue_time``, ``max_age`` that expect an integer value.
+ See https://github.com/Pylons/pyramid/pull/2050
+
+Bug Fixes
+---------
+
+- ``pyramid.httpexceptions.HTTPException`` now defaults to
+ ``520 Unknown Error`` instead of ``None None`` to conform with changes in
+ WebOb 1.5.
+ See https://github.com/Pylons/pyramid/pull/1865
+
+- ``pshell`` will now preserve the capitalization of variables in the
+ ``[pshell]`` section of the INI file. This makes exposing classes to the
+ shell a little more straightfoward.
+ See https://github.com/Pylons/pyramid/pull/1883
+
+- Fixed usage of ``pserve --monitor-restart --daemon`` which would fail in
+ horrible ways. See https://github.com/Pylons/pyramid/pull/2118
+
+- Explicitly prevent ``pserve --reload --daemon`` from being used. It's never
+ been supported but would work and fail in weird ways.
+ See https://github.com/Pylons/pyramid/pull/2119
+
+- Fix an issue on Windows when running ``pserve --reload`` in which the
+ process failed to fork because it could not find the pserve script to
+ run. See https://github.com/Pylons/pyramid/pull/2138
+
+Deprecations
+------------
+
+- Deprecate ``pserve --monitor-restart`` in favor of user's using a real
+ process manager such as Systemd or Upstart as well as Python-based
+ solutions like Circus and Supervisor.
+ See https://github.com/Pylons/pyramid/pull/2120
+
+1.6a2 (2015-06-30)
+==================
+
+Bug Fixes
+---------
+
+- Ensure that ``pyramid.httpexceptions.exception_response`` returns the
+ appropriate "concrete" class for ``400`` and ``500`` status codes.
+ See https://github.com/Pylons/pyramid/issues/1832
+
+- Fix an infinite recursion bug introduced in 1.6a1 when
+ ``pyramid.view.render_view_to_response`` was called directly or indirectly.
+ See https://github.com/Pylons/pyramid/issues/1643
+
+- Further fix the JSONP renderer by prefixing the returned content with
+ a comment. This should mitigate attacks from Flash (See CVE-2014-4671).
+ See https://github.com/Pylons/pyramid/pull/1649
+
+- Allow periods and brackets (``[]``) in the JSONP callback. The original
+ fix was overly-restrictive and broke Angular.
+ See https://github.com/Pylons/pyramid/pull/1649
+
+1.6a1 (2015-04-15)
+==================
+
+Features
+--------
+
+- pcreate will now ask for confirmation if invoked with
+ an argument for a project name that already exists or
+ is importable in the current environment.
+ See https://github.com/Pylons/pyramid/issues/1357 and
+ https://github.com/Pylons/pyramid/pull/1837
+
+- Make it possible to subclass ``pyramid.request.Request`` and also use
+ ``pyramid.request.Request.add_request.method``. See
+ https://github.com/Pylons/pyramid/issues/1529
+
+- The ``pyramid.config.Configurator`` has grown the ability to allow
+ actions to call other actions during a commit-cycle. This enables much more
+ logic to be placed into actions, such as the ability to invoke other actions
+ or group them for improved conflict detection. We have also exposed and
+ documented the config phases that Pyramid uses in order to further assist
+ in building conforming addons.
+ See https://github.com/Pylons/pyramid/pull/1513
+
+- Add ``pyramid.request.apply_request_extensions`` function which can be
+ used in testing to apply any request extensions configured via
+ ``config.add_request_method``. Previously it was only possible to test
+ the extensions by going through Pyramid's router.
+ See https://github.com/Pylons/pyramid/pull/1581
+
+- pcreate when run without a scaffold argument will now print information on
+ the missing flag, as well as a list of available scaffolds.
+ See https://github.com/Pylons/pyramid/pull/1566 and
+ https://github.com/Pylons/pyramid/issues/1297
+
+- Added support / testing for 'pypy3' under Tox and Travis.
+ See https://github.com/Pylons/pyramid/pull/1469
+
+- Automate code coverage metrics across py2 and py3 instead of just py2.
+ See https://github.com/Pylons/pyramid/pull/1471
+
+- Cache busting for static resources has been added and is available via a new
+ argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``.
+ Core APIs are shipped for both cache busting via query strings and
+ path segments and may be extended to fit into custom asset pipelines.
+ See https://github.com/Pylons/pyramid/pull/1380 and
+ https://github.com/Pylons/pyramid/pull/1583
+
+- Add ``pyramid.config.Configurator.root_package`` attribute and init
+ parameter to assist with includeable packages that wish to resolve
+ resources relative to the package in which the ``Configurator`` was created.
+ This is especially useful for addons that need to load asset specs from
+ settings, in which case it is may be natural for a developer to define
+ imports or assets relative to the top-level package.
+ See https://github.com/Pylons/pyramid/pull/1337
+
+- Added line numbers to the log formatters in the scaffolds to assist with
+ debugging. See https://github.com/Pylons/pyramid/pull/1326
+
+- Add new HTTP exception objects for status codes
+ ``428 Precondition Required``, ``429 Too Many Requests`` and
+ ``431 Request Header Fields Too Large`` in ``pyramid.httpexceptions``.
+ See https://github.com/Pylons/pyramid/pull/1372/files
+
+- The ``pshell`` script will now load a ``PYTHONSTARTUP`` file if one is
+ defined in the environment prior to launching the interpreter.
+ See https://github.com/Pylons/pyramid/pull/1448
+
+- Make it simple to define notfound and forbidden views that wish to use
+ the default exception-response view but with altered predicates and other
+ configuration options. The ``view`` argument is now optional in
+ ``config.add_notfound_view`` and ``config.add_forbidden_view``..
+ See https://github.com/Pylons/pyramid/issues/494
+
+- Greatly improve the readability of the ``pcreate`` shell script output.
+ See https://github.com/Pylons/pyramid/pull/1453
+
+- Improve robustness to timing attacks in the ``AuthTktCookieHelper`` and
+ the ``SignedCookieSessionFactory`` classes by using the stdlib's
+ ``hmac.compare_digest`` if it is available (such as Python 2.7.7+ and 3.3+).
+ See https://github.com/Pylons/pyramid/pull/1457
+
+- Assets can now be overidden by an absolute path on the filesystem when using
+ the ``config.override_asset`` API. This makes it possible to fully support
+ serving up static content from a mutable directory while still being able
+ to use the ``request.static_url`` API and ``config.add_static_view``.
+ Previously it was not possible to use ``config.add_static_view`` with an
+ absolute path **and** generate urls to the content. This change replaces
+ the call, ``config.add_static_view('/abs/path', 'static')``, with
+ ``config.add_static_view('myapp:static', 'static')`` and
+ ``config.override_asset(to_override='myapp:static/',
+ override_with='/abs/path/')``. The ``myapp:static`` asset spec is completely
+ made up and does not need to exist - it is used for generating urls
+ via ``request.static_url('myapp:static/foo.png')``.
+ See https://github.com/Pylons/pyramid/issues/1252
+
+- Added ``pyramid.config.Configurator.set_response_factory`` and the
+ ``response_factory`` keyword argument to the ``Configurator`` for defining
+ a factory that will return a custom ``Response`` class.
+ See https://github.com/Pylons/pyramid/pull/1499
+
+- Allow an iterator to be returned from a renderer. Previously it was only
+ possible to return bytes or unicode.
+ See https://github.com/Pylons/pyramid/pull/1417
+
+- ``pserve`` can now take a ``-b`` or ``--browser`` option to open the server
+ URL in a web browser. See https://github.com/Pylons/pyramid/pull/1533
+
+- Overall improvments for the ``proutes`` command. Added ``--format`` and
+ ``--glob`` arguments to the command, introduced the ``method``
+ column for displaying available request methods, and improved the ``view``
+ output by showing the module instead of just ``__repr__``.
+ See https://github.com/Pylons/pyramid/pull/1488
+
+- Support keyword-only arguments and function annotations in views in
+ Python 3. See https://github.com/Pylons/pyramid/pull/1556
+
+- ``request.response`` will no longer be mutated when using the
+ ``pyramid.renderers.render_to_response()`` API. It is now necessary to
+ pass in a ``response=`` argument to ``render_to_response`` if you wish to
+ supply the renderer with a custom response object for it to use. If you
+ do not pass one then a response object will be created using the
+ application's ``IResponseFactory``. Almost all renderers
+ mutate the ``request.response`` response object (for example, the JSON
+ renderer sets ``request.response.content_type`` to ``application/json``).
+ However, when invoking ``render_to_response`` it is not expected that the
+ response object being returned would be the same one used later in the
+ request. The response object returned from ``render_to_response`` is now
+ explicitly different from ``request.response``. This does not change the
+ API of a renderer. See https://github.com/Pylons/pyramid/pull/1563
+
+- The ``append_slash`` argument of ```Configurator().add_notfound_view()`` will
+ now accept anything that implements the ``IResponse`` interface and will use
+ that as the response class instead of the default ``HTTPFound``. See
+ https://github.com/Pylons/pyramid/pull/1610
+
+Bug Fixes
+---------
+
+- The JSONP renderer created JavaScript code in such a way that a callback
+ variable could be used to arbitrarily inject javascript into the response
+ object. https://github.com/Pylons/pyramid/pull/1627
+
+- Work around an issue where ``pserve --reload`` would leave terminal echo
+ disabled if it reloaded during a pdb session.
+ See https://github.com/Pylons/pyramid/pull/1577,
+ https://github.com/Pylons/pyramid/pull/1592
+
+- ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise
+ ``ValueError`` when accidentally passed ``None``.
+ See https://github.com/Pylons/pyramid/pull/1320
+
+- Fix an issue whereby predicates would be resolved as maybe_dotted in the
+ introspectable but not when passed for registration. This would mean that
+ ``add_route_predicate`` for example can not take a string and turn it into
+ the actual callable function.
+ See https://github.com/Pylons/pyramid/pull/1306
+
+- Fix ``pyramid.testing.setUp`` to return a ``Configurator`` with a proper
+ package. Previously it was not possible to do package-relative includes
+ using the returned ``Configurator`` during testing. There is now a
+ ``package`` argument that can override this behavior as well.
+ See https://github.com/Pylons/pyramid/pull/1322
+
+- Fix an issue where a ``pyramid.response.FileResponse`` may apply a charset
+ where it does not belong. See https://github.com/Pylons/pyramid/pull/1251
+
+- Work around a bug introduced in Python 2.7.7 on Windows where
+ ``mimetypes.guess_type`` returns Unicode rather than str for the content
+ type, unlike any previous version of Python. See
+ https://github.com/Pylons/pyramid/issues/1360 for more information.
+
+- ``pcreate`` now normalizes the package name by converting hyphens to
+ underscores. See https://github.com/Pylons/pyramid/pull/1376
+
+- Fix an issue with the final response/finished callback being unable to
+ add another callback to the list. See
+ https://github.com/Pylons/pyramid/pull/1373
+
+- Fix a failing unittest caused by differing mimetypes across various OSs.
+ See https://github.com/Pylons/pyramid/issues/1405
+
+- Fix route generation for static view asset specifications having no path.
+ See https://github.com/Pylons/pyramid/pull/1377
+
+- Allow the ``pyramid.renderers.JSONP`` renderer to work even if there is no
+ valid request object. In this case it will not wrap the object in a
+ callback and thus behave just like the ``pyramid.renderers.JSON`` renderer.
+ See https://github.com/Pylons/pyramid/pull/1561
+
+- Prevent "parameters to load are deprecated" ``DeprecationWarning``
+ from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541
+
+- Avoiding sharing the ``IRenderer`` objects across threads when attached to
+ a view using the `renderer=` argument. These renderers were instantiated
+ at time of first render and shared between requests, causing potentially
+ subtle effects like `pyramid.reload_templates = true` failing to work
+ in `pyramid_mako`. See https://github.com/Pylons/pyramid/pull/1575
+ and https://github.com/Pylons/pyramid/issues/1268
+
+- Avoiding timing attacks against CSRF tokens.
+ See https://github.com/Pylons/pyramid/pull/1574
+
+- ``request.finished_callbacks`` and ``request.response_callbacks`` now
+ default to an iterable instead of ``None``. It may be checked for a length
+ of 0. This was the behavior in 1.5.
+
+Deprecations
+------------
+
+- The ``pserve`` command's daemonization features have been deprecated. This
+ includes the ``[start,stop,restart,status]`` subcommands as well as the
+ ``--daemon``, ``--stop-server``, ``--pid-file``, and ``--status`` flags.
+
+ Please use a real process manager in the future instead of relying on the
+ ``pserve`` to daemonize itself. Many options exist including your Operating
+ System's services such as Systemd or Upstart, as well as Python-based
+ solutions like Circus and Supervisor.
+
+ See https://github.com/Pylons/pyramid/pull/1641
+
+- Renamed the ``principal`` argument to ``pyramid.security.remember()`` to
+ ``userid`` in order to clarify its intended purpose.
+ See https://github.com/Pylons/pyramid/pull/1399
+
+Docs
+----
+
+- Moved the documentation for ``accept`` on ``Configurator.add_view`` to no
+ longer be part of the predicate list. See
+ https://github.com/Pylons/pyramid/issues/1391 for a bug report stating
+ ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the
+ conclusion that it should not be documented as a predicate.
+ See https://github.com/Pylons/pyramid/pull/1487 for this PR
+
+- Removed logging configuration from Quick Tutorial ini files except for
+ scaffolding- and logging-related chapters to avoid needing to explain it too
+ early.
+
+- Clarify a previously-implied detail of the ``ISession.invalidate`` API
+ documentation.
+
+- Improve and clarify the documentation on what Pyramid defines as a
+ ``principal`` and a ``userid`` in its security APIs.
+ See https://github.com/Pylons/pyramid/pull/1399
+
+- Add documentation of command line programs (``p*`` scripts). See
+ https://github.com/Pylons/pyramid/pull/2191
+
+Scaffolds
+---------
+
+- Update scaffold generating machinery to return the version of pyramid and
+ pyramid docs for use in scaffolds. Updated starter, alchemy and zodb
+ templates to have links to correctly versioned documentation and reflect
+ which pyramid was used to generate the scaffold.
+
+- Removed non-ascii copyright symbol from templates, as this was
+ causing the scaffolds to fail for project generation.
+
+- You can now run the scaffolding func tests via ``tox py2-scaffolds`` and
+ ``tox py3-scaffolds``.
+
+
1.5 (2014-04-08)
================
diff --git a/RELEASING.txt b/RELEASING.txt
index 75a4fcea2..866fff305 100644
--- a/RELEASING.txt
+++ b/RELEASING.txt
@@ -1,7 +1,18 @@
Releasing Pyramid
=================
-- Do any necessary branch merges (e.g. master to branch, branch to master).
+- For clarity, we define releases as follows.
+
+ - Alpha, beta, dev and similar statuses do not qualify whether a release is
+ major or minor. The term "pre-release" means alpha, beta, or dev.
+
+ - A *major* release is where the first number either before or after the
+ first dot increases. Examples: 1.6 to 1.7a1, or 1.8 to 2.0.
+
+ - A *minor* or *bug fix* release is where the number after the second dot
+ increases. Example: 1.6 to 1.6.1.
+
+- Do any necessary branch merges (e.g., master to branch, branch to master).
- On release branch:
@@ -15,8 +26,8 @@ Releasing Pyramid
- Run tests on Windows if feasible.
-- Make sure all scaffold tests pass (Py 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, pypy, and
- pypy3 on UNIX; this doesn't work on Windows):
+- Make sure all scaffold tests pass (Py 2.7, 3.3, 3.4, 3.5, pypy, and pypy3 on
+ UNIX; this doesn't work on Windows):
$ ./scaffoldtests.sh
@@ -26,7 +37,7 @@ Releasing Pyramid
- Copy relevant changes (delta bug fixes) from CHANGES.txt to
docs/whatsnew-X.X (if it's a major release). Minor releases should
include a link under "Bug Fix Releases" to the minor feature
- changes in CHANGES.txt .
+ changes in CHANGES.txt.
- Update README.rst to use correct versions of badges and URLs according to
each branch and context, i.e., RTD "latest" == GitHub/Travis "1.x-branch".
@@ -34,12 +45,15 @@ Releasing Pyramid
- Update whatsnew-X.X.rst in docs to point at change log entries for individual
releases if applicable.
+- For major version releases, in contributing.md, update branch descriptions.
+
- For major version releases, in docs/conf.py, update values under
html_theme_options for in_progress and outdated across master, releasing
branch, and previously released branch. Also in the previously released
branch only, uncomment the sections to enable pylons_sphinx_latesturl.
-- Change setup.py version to the new version number.
+- Change setup.py version to the new version number on both master and the new
+ branch.
- Change CHANGES.txt heading to reflect the new version number.
@@ -50,7 +64,7 @@ Releasing Pyramid
- Create a release tag.
-- Make sure your Python has ``setuptools-git``, ``twine`` and ``wheel``
+- Make sure your Python has ``setuptools-git``, ``twine``, and ``wheel``
installed and release to PyPI::
$ python setup.py sdist bdist_wheel
diff --git a/TODO.txt b/TODO.txt
index 837c9d681..797f8acef 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -120,10 +120,8 @@ Future
- 1.6: Remove IContextURL and TraversalContextURL.
-- 1.7: Change ``pyramid.authentication.AuthTktAuthenticationPolicy`` default
- ``hashalg`` to ``sha512``.
-
- 1.8: Remove set_request_property.
+- 1.8: Drop Python 3.3 support.
- 1.9: Remove extra code enabling ``pyramid.security.remember(principal=...)``
and force use of ``userid``.
diff --git a/contributing.md b/contributing.md
index c2d2ecefd..af19ed093 100644
--- a/contributing.md
+++ b/contributing.md
@@ -27,11 +27,8 @@ listed below.
* [master](https://github.com/Pylons/pyramid/) - The branch on which further
development takes place. The default branch on GitHub.
* [1.6-branch](https://github.com/Pylons/pyramid/tree/1.6-branch) - The branch
-to which further development on master should be backported. This is also a
-development branch.
-* [1.5-branch](https://github.com/Pylons/pyramid/tree/1.5-branch) - The branch
-classified as "stable" or "latest". Actively maintained.
-* [1.4-branch](https://github.com/Pylons/pyramid/tree/1.4-branch) - The oldest
+classified as "stable" or "latest". Actively maintained.
+* [1.5-branch](https://github.com/Pylons/pyramid/tree/1.5-branch) - The oldest
actively maintained and stable branch.
Older branches are not actively maintained. In general, two stable branches and
diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle
index 71319610b..56e4e13f2 100644
--- a/docs/_static/pyramid_request_processing.graffle
+++ b/docs/_static/pyramid_request_processing.graffle
@@ -22,7 +22,7 @@
<key>Font</key>
<string>Helvetica</string>
<key>Size</key>
- <real>12</real>
+ <real>13</real>
</dict>
<key>ID</key>
<integer>2</integer>
@@ -53,12 +53,187 @@
<key>Creator</key>
<string>Steve Piercy</string>
<key>DisplayScale</key>
- <string>1 0/72 in = 1 0/72 in</string>
+ <string>1 0/72 in = 1.0000 in</string>
<key>GraphDocumentVersion</key>
<integer>8</integer>
<key>GraphicsList</key>
<array>
<dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 294.65604172230951}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169515</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{0.50000000000000089, -0.49999999999999645}</string>
+ <string>{-0.49526813868737474, -0.4689979626999552}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 CSRF checks}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169513</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169514</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Points</key>
+ <array>
+ <string>{154.9999760464211, 209.11365574251681}</string>
+ <string>{239.8333613077798, 209.14732074737549}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>2</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169373</integer>
+ <key>Position</key>
+ <real>0.47711458802223206</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{239.83336130777977, 197.875}, {105.66666412353516, 22.544641494750977}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169513</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.999449</string>
+ <key>g</key>
+ <string>0.743511</string>
+ <key>r</key>
+ <string>0.872276</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 BeforeTraversal}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
<key>Class</key>
<string>LineGraphic</string>
<key>FontInfo</key>
@@ -84,8 +259,8 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{344.41667175292969, 402.88506673894034}</string>
- <string>{375.5, 402.27232108797347}</string>
+ <string>{344.41668319702148, 411.88506673894034}</string>
+ <string>{375.5, 411.77232108797347}</string>
</array>
<key>Style</key>
<dict>
@@ -113,7 +288,7 @@
<key>Tail</key>
<dict>
<key>ID</key>
- <integer>169428</integer>
+ <integer>169509</integer>
</dict>
</dict>
<dict>
@@ -237,378 +412,430 @@
</dict>
</dict>
<dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 275.99999999999994}, {105.75002924601222, 18.656048080136394}}</string>
<key>Class</key>
- <string>Group</string>
- <key>Graphics</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169506</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
<array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{0.50000000000000089, -0.49999999999999645}</string>
+ <string>{-0.49526813868737474, -0.4689979626999552}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.8333613077798, 284.99999999999994}, {105.66668701171875, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169425</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- <string>{0.50000000000000089, -0.49999999999999645}</string>
- <string>{-0.49526813868737474, -0.4689979626999552}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 authorization}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 421.15071036499205}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169507</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{0.50000000000000089, 0.5}</string>
+ <string>{-0.49999999999999911, 0.49999999999999289}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.75000762939453, 412.15071036499205}, {105.66666412353516, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169426</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- <string>{0.50000000000000089, 0.5}</string>
- <string>{-0.49999999999999911, 0.49999999999999289}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 decorators egress}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 312.65604172230951}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169508</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ <string>{0.50000000000000089, -0.49999999999999645}</string>
+ <string>{-0.49526813868737474, -0.4689979626999552}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.75000762939453, 303.65604172230951}, {105.66666412353516, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169427</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- <string>{0.50000000000000089, -0.49999999999999645}</string>
- <string>{-0.49526813868737474, -0.4689979626999552}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 decorators ingress}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 402.55704269887212}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169509</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.75000762939453, 393.55704269887212}, {105.66666412353516, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169428</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 response adapter}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 383.90099016834085}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169510</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.75000762939453, 374.90099016834085}, {105.66666412353516, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169429</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 view mapper egress}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 350.36561209044055}, {105.66668701171875, 33.089282989501953}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169511</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.75000762939453, 341.36561209044055}, {105.66666412353516, 33.089282989501953}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169430</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.422927</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.422927</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 view}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{238.74999618530273, 331.26348241170439}, {105.66668701171875, 18.656048080136394}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>11</real>
+ </dict>
+ <key>ID</key>
+ <integer>169512</integer>
+ <key>Layer</key>
+ <integer>0</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
<dict>
- <key>Bounds</key>
- <string>{{238.75000762939453, 322.26348241170439}, {105.66666412353516, 18.656048080136394}}</string>
- <key>Class</key>
- <string>ShapedGraphic</string>
- <key>ID</key>
- <integer>169431</integer>
- <key>Magnets</key>
- <array>
- <string>{0, 1}</string>
- <string>{0, -1}</string>
- <string>{1, 0}</string>
- <string>{-1, 0}</string>
- </array>
- <key>Shape</key>
- <string>Rectangle</string>
- <key>Style</key>
+ <key>Color</key>
<dict>
- <key>fill</key>
- <dict>
- <key>Color</key>
- <dict>
- <key>b</key>
- <string>0.637876</string>
- <key>g</key>
- <string>1</string>
- <key>r</key>
- <string>1</string>
- </dict>
- </dict>
- <key>shadow</key>
- <dict>
- <key>Draws</key>
- <string>NO</string>
- <key>ShadowVector</key>
- <string>{2, 2}</string>
- </dict>
+ <key>b</key>
+ <string>0.637876</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
</dict>
- <key>Text</key>
- <dict>
- <key>Text</key>
- <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>ShadowVector</key>
+ <string>{2, 2}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
\f0\fs20 \cf0 view mapper ingress}</string>
- <key>VerticalPad</key>
- <integer>0</integer>
- </dict>
- </dict>
- </array>
- <key>ID</key>
- <integer>169424</integer>
- <key>Layer</key>
- <integer>0</integer>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
</dict>
<dict>
<key>Class</key>
@@ -1094,7 +1321,7 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{238.75000762939462, 430.80675844512831}</string>
+ <string>{238.74999618530282, 439.80675844512831}</string>
<string>{207.66666666666765, 385.656005859375}</string>
</array>
<key>Style</key>
@@ -1123,7 +1350,7 @@
<key>Tail</key>
<dict>
<key>ID</key>
- <integer>169426</integer>
+ <integer>169507</integer>
<key>Info</key>
<integer>6</integer>
</dict>
@@ -1144,7 +1371,7 @@
<integer>0</integer>
<key>Points</key>
<array>
- <string>{239.33336141608385, 285.57837549845181}</string>
+ <string>{239.25039065750093, 276.57837549845181}</string>
<string>{207.66666666666777, 353.07514659563753}</string>
</array>
<key>Style</key>
@@ -1173,7 +1400,7 @@
<key>Tail</key>
<dict>
<key>ID</key>
- <integer>169425</integer>
+ <integer>169506</integer>
<key>Info</key>
<integer>6</integer>
</dict>
@@ -1515,7 +1742,7 @@
{\colortbl;\red255\green255\blue255;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
-\f0\fs20 \cf0 view}</string>
+\f0\fs20 \cf0 view deriver}</string>
<key>VerticalPad</key>
<integer>0</integer>
</dict>
@@ -1742,7 +1969,7 @@
</dict>
<dict>
<key>Bounds</key>
- <string>{{375.5, 391}, {105.66666412353516, 22.544642175946908}}</string>
+ <string>{{375.5, 400.5}, {105.66666412353516, 22.544642175946908}}</string>
<key>Class</key>
<string>ShapedGraphic</string>
<key>ID</key>
@@ -9578,6 +9805,10 @@
<string>YES</string>
<key>HPages</key>
<integer>1</integer>
+ <key>HorizontalGuides</key>
+ <array>
+ <real>209.875</real>
+ </array>
<key>ImageCounter</key>
<integer>3</integer>
<key>KeepToScale</key>
@@ -9637,7 +9868,7 @@
<key>MasterSheets</key>
<array/>
<key>ModificationDate</key>
- <string>2014-11-23 07:19:11 +0000</string>
+ <string>2016-04-13 08:32:47 +0000</string>
<key>Modifier</key>
<string>Steve Piercy</string>
<key>NotesVisible</key>
@@ -9718,7 +9949,7 @@
</dict>
</array>
<key>Frame</key>
- <string>{{35, 93}, {1394, 1325}}</string>
+ <string>{{35, 93}, {2284, 1325}}</string>
<key>ListView</key>
<true/>
<key>OutlineWidth</key>
@@ -9732,15 +9963,15 @@
<key>SidebarWidth</key>
<integer>163</integer>
<key>VisibleRegion</key>
- <string>{{-231, -226}, {1037, 1186}}</string>
+ <string>{{110.125, 77.875}, {239.125, 146.375}}</string>
<key>Zoom</key>
- <real>1</real>
+ <real>8</real>
<key>ZoomValues</key>
<array>
<array>
<string>Request Processing</string>
- <real>1</real>
- <real>2</real>
+ <real>8</real>
+ <real>4</real>
</array>
</array>
</dict>
diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png
index 2fbb1e164..2f44f4824 100644
--- a/docs/_static/pyramid_request_processing.png
+++ b/docs/_static/pyramid_request_processing.png
Binary files differ
diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg
index 21bbcb532..03f6d56fa 100644
--- a/docs/_static/pyramid_request_processing.svg
+++ b/docs/_static/pyramid_request_processing.svg
@@ -1,3 +1,3 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="91 11 424 533" width="424pt" height="533pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2014-11-23 07:19Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Request Processing</title><rect fill="white" width="576" height="733"/><g><title>no exceptions</title><path d="M 155 444.75674 C 155 450.64061 155 486.2592 155 502.71617" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 322.33334 C 154.99999 327.72413 155 337.74646 155 346.1775" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 245.22768 C 154.99999 250.5417 154.99999 257.93189 154.99999 265.10145" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99995 198.62203 C 154.99995 203.74682 154.99998 209.1909 154.99999 215.28222" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 50.455358)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="4.7596016" y="10" textLength="88.92578">middleware ingress </tspan></text><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 101.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.983723" y="10" textLength="61.69922">tween ingress</tspan></text><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 227.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="28.660969" y="10" textLength="38.344727">traversal</tspan></text><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 252.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.424641" y="10" textLength="62.817383">ContextFound</tspan></text><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 427.48442)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.094563" y="10" textLength="59.47754">tween egress</tspan></text><rect x="239" y="445.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="445.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 450.50821)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3113594" y="10" textLength="85.043945">response callbacks</tspan></text><rect x="239" y="497.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="497.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 502.5082)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.6463203" y="10" textLength="5">fi</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="13.64632" y="10" textLength="73.374023">nished callbacks</tspan></text><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 514.89027)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.8704414" y="10" textLength="83.92578">middleware egress</tspan></text><path d="M 155 67.72768 C 155 73.048893 155 81.55558 155 89.2853" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 155 119.22768 C 155 124.62026 154.99997 133.48763 154.99996 141.38632" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="375.5" y="391" width="105.666664" height="22.544642" fill="#dfbeff"/><rect x="375.5" y="391" width="105.666664" height="22.544642" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(380.5 396.27232)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.702961" y="10" textLength="62.260742">BeforeRender</tspan></text><text transform="translate(233.5 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".31445312" y="11" textLength="115.371094">Request Processing</tspan></text><path d="M 375.99995 42.910746 L 498.66662 42.910746 C 501.42805 42.910746 503.66662 45.149323 503.66662 47.910746 L 503.66662 222 C 503.66662 224.76142 501.42805 227 498.66662 227 L 375.99995 227 C 373.23853 227 370.99995 224.76142 370.99995 222 L 370.99995 47.910746 C 370.99995 45.149323 373.23853 42.910746 375.99995 42.910746 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(375.99995 42.910746)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="35.55664">Legend</tspan></text><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 69.180834)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.601887" y="10" textLength="24.46289">event</tspan></text><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" fill="#fed153"/><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 191.85458)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.769367" y="10" textLength="36.12793">callback</tspan></text><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" fill="#ffff6c"/><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 163.8223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.83089" y="10" textLength="20.004883">view</tspan></text><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" fill="#a4cfff"/><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 96.48543)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.148762" y="10" textLength="76.14746">external process </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="2.8162422" y="22" textLength="90.03418">(middleware, tween)</tspan></text><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 135.79003)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.537922" y="10" textLength="70.59082">internal process</tspan></text><line x1="154.99999" y1="258.44082" x2="238.83336" y2="258.45536" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 363.61979)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.205402" y="10" textLength="57.25586">view pipeline</tspan></text><path d="M 155 386.66443 C 155 392.17252 155 405.5052 155 415.30935" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="239.33336" y1="285.57838" x2="207.66667" y2="353.07515" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><line x1="238.75001" y1="430.80676" x2="207.66667" y2="385.656" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" fill="#d2ffd0"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 307.71132)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="24.764484" y="10" textLength="46.137695">predicates</tspan></text><rect x="102.16666" y="272" width="105.666664" height="33.089294" fill="#d2ffd0"/><rect x="102.16666" y="272" width="105.666664" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 282.54465)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.707844" y="10" textLength="52.250977">view lookup</tspan></text><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" fill="#d2ffd0"/><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 184)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.978855" y="10" textLength="71.708984">route predicates</tspan></text><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" fill="#d2ffd0"/><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 158.83333)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.001804" y="10" textLength="20.004883">URL</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.640476" y="10" textLength="40.024414"> dispatch</tspan></text><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.8334 122.59152)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.207844" y="10" textLength="57.250977">NewRequest</tspan></text><line x1="154.99999" y1="128.68025" x2="239.8334" y2="128.59152" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 476.49852)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="15.316242" y="10" textLength="65.03418">NewResponse</tspan></text><line x1="155" y1="470.25295" x2="238.33861" y2="482.42625" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75001" y="322.26348" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="322.26348" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 325.5915)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="1.9812813" y="10" textLength="91.7041">view mapper ingress</tspan></text><rect x="238.75001" y="341.36561" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="238.75001" y="341.36561" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 351.91025)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.83089" y="10" textLength="20.004883">view</tspan></text><rect x="238.75001" y="374.901" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="374.901" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 378.22901)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="3.0921211" y="10" textLength="89.48242">view mapper egress</tspan></text><rect x="238.75001" y="393.55704" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="393.55704" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 396.88507)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.9173164" y="10" textLength="77.83203">response adapter</tspan></text><rect x="238.75001" y="303.65604" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="303.65604" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 306.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="6.702961" y="10" textLength="82.26074">decorators ingress</tspan></text><rect x="238.75001" y="412.1507" width="105.666664" height="18.656048" fill="#ffffa3"/><rect x="238.75001" y="412.1507" width="105.666664" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75001 415.47873)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="7.813801" y="10" textLength="80.039062">decorators egress</tspan></text><rect x="238.83336" y="285" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.83336" y="285" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 288.32802)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.202972" y="10" textLength="57.260742">authorization</tspan></text><line x1="155" y1="482.12575" x2="238.52297" y2="508.3584" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="155" y1="459.27668" x2="238.50027" y2="456.52468" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="344.41667" y1="402.88507" x2="375.5" y2="402.27232" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/></g></g></svg>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="91 11 424 533" width="424pt" height="533pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2016-04-13 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Request Processing</title><rect fill="white" width="576" height="733"/><g><title>no exceptions</title><path d="M 155 444.75674 C 155 450.64061 155 486.2592 155 502.71617" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 322.33334 C 154.99999 327.72413 155 337.74646 155 346.1775" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 245.22768 C 154.99999 250.5417 154.99999 257.93189 154.99999 265.10145" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99995 198.62203 C 154.99995 203.74682 154.99998 209.1909 154.99999 215.28222" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 50.455358)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="4.7596016" y="10" textLength="88.92578">middleware ingress </tspan></text><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 101.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.983723" y="10" textLength="61.69922">tween ingress</tspan></text><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 227.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="28.660969" y="10" textLength="38.344727">traversal</tspan></text><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 252.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.424641" y="10" textLength="62.817383">ContextFound</tspan></text><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 427.48442)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.094563" y="10" textLength="59.47754">tween egress</tspan></text><rect x="239" y="445.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="445.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 450.50821)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3113594" y="10" textLength="85.043945">response callbacks</tspan></text><rect x="239" y="497.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="497.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 502.5082)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.6463203" y="10" textLength="5">fi</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="13.64632" y="10" textLength="73.374023">nished callbacks</tspan></text><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 514.89027)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.8704414" y="10" textLength="83.92578">middleware egress</tspan></text><path d="M 155 67.72768 C 155 73.048893 155 81.55558 155 89.2853" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 155 119.22768 C 155 124.62026 154.99997 133.48763 154.99996 141.38632" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="375.5" y="400.5" width="105.666664" height="22.544642" fill="#dfbeff"/><rect x="375.5" y="400.5" width="105.666664" height="22.544642" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(380.5 405.77232)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.702961" y="10" textLength="62.260742">BeforeRender</tspan></text><text transform="translate(233.5 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".31445312" y="11" textLength="115.371094">Request Processing</tspan></text><path d="M 375.99995 42.910746 L 498.66662 42.910746 C 501.42805 42.910746 503.66662 45.149323 503.66662 47.910746 L 503.66662 222 C 503.66662 224.76142 501.42805 227 498.66662 227 L 375.99995 227 C 373.23853 227 370.99995 224.76142 370.99995 222 L 370.99995 47.910746 C 370.99995 45.149323 373.23853 42.910746 375.99995 42.910746 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(375.99995 42.910746)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="35.55664">Legend</tspan></text><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 69.180834)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.601887" y="10" textLength="24.46289">event</tspan></text><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" fill="#fed153"/><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 191.85458)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.769367" y="10" textLength="36.12793">callback</tspan></text><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" fill="#ffff6c"/><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 163.8223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.158527" y="10" textLength="53.34961">view deriver</tspan></text><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" fill="#a4cfff"/><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 96.48543)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.148762" y="10" textLength="76.14746">external process </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="2.8162422" y="22" textLength="90.03418">(middleware, tween)</tspan></text><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 135.79003)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.537922" y="10" textLength="70.59082">internal process</tspan></text><line x1="154.99999" y1="258.44082" x2="238.83336" y2="258.45536" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 363.61979)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.205402" y="10" textLength="57.25586">view pipeline</tspan></text><path d="M 155 386.66443 C 155 392.17252 155 405.5052 155 415.30935" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="239.25039" y1="276.57838" x2="207.66667" y2="353.07515" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><line x1="238.75" y1="439.80676" x2="207.66667" y2="385.656" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" fill="#d2ffd0"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 307.71132)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="24.764484" y="10" textLength="46.137695">predicates</tspan></text><rect x="102.16666" y="272" width="105.666664" height="33.089294" fill="#d2ffd0"/><rect x="102.16666" y="272" width="105.666664" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 282.54465)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.707844" y="10" textLength="52.250977">view lookup</tspan></text><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" fill="#d2ffd0"/><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 184)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.978855" y="10" textLength="71.708984">route predicates</tspan></text><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" fill="#d2ffd0"/><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 158.83333)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.001804" y="10" textLength="20.004883">URL</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.640476" y="10" textLength="40.024414"> dispatch</tspan></text><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.8334 122.59152)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.207844" y="10" textLength="57.250977">NewRequest</tspan></text><line x1="154.99999" y1="128.68025" x2="239.8334" y2="128.59152" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 476.49852)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="15.316242" y="10" textLength="65.03418">NewResponse</tspan></text><line x1="155" y1="470.25295" x2="238.33861" y2="482.42625" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="331.26348" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="331.26348" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 334.5915)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="1.9812927" y="10" textLength="91.7041">view mapper ingress</tspan></text><rect x="238.75" y="350.36561" width="105.66669" height="33.089283" fill="#ffff6c"/><rect x="238.75" y="350.36561" width="105.66669" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 360.91025)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.830902" y="10" textLength="20.004883">view</tspan></text><rect x="238.75" y="383.901" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="383.901" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 387.22901)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="3.0921326" y="10" textLength="89.48242">view mapper egress</tspan></text><rect x="238.75" y="402.55704" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="402.55704" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 405.88507)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.917328" y="10" textLength="77.83203">response adapter</tspan></text><rect x="238.75" y="312.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="312.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 315.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="6.7029724" y="10" textLength="82.26074">decorators ingress</tspan></text><rect x="238.75" y="421.1507" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="421.1507" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 424.47873)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="7.8138123" y="10" textLength="80.039062">decorators egress</tspan></text><rect x="238.75" y="276" width="105.75003" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="276" width="105.75003" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 279.32802)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.244644" y="10" textLength="57.260742">authorization</tspan></text><line x1="155" y1="482.12575" x2="238.52297" y2="508.3584" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="155" y1="459.27668" x2="238.50027" y2="456.52468" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="344.41668" y1="411.88507" x2="375.5" y2="411.77232" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="239.83336" y="197.875" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.83336" y="197.875" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.83336 203.14732)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.44759" y="10" textLength="35.57129">BeforeT</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="47.652668" y="10" textLength="35.566406">raversal</tspan></text><line x1="154.99998" y1="209.11366" x2="239.83336" y2="209.14732" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="294.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="294.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 297.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="17.27182" y="10" textLength="61.123047">CSRF checks</tspan></text></g></g></svg>
diff --git a/docs/api/config.rst b/docs/api/config.rst
index ae913d32c..e083dbc68 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -66,6 +66,7 @@
.. automethod:: add_tween
.. automethod:: add_route_predicate
.. automethod:: add_view_predicate
+ .. automethod:: add_view_deriver
.. automethod:: set_request_factory
.. automethod:: set_root_factory
.. automethod:: set_session_factory
diff --git a/docs/api/events.rst b/docs/api/events.rst
index 31a0e22c1..0a8463740 100644
--- a/docs/api/events.rst
+++ b/docs/api/events.rst
@@ -21,6 +21,8 @@ Event Types
.. autoclass:: ContextFound
+.. autoclass:: BeforeTraversal
+
.. autoclass:: NewResponse
.. autoclass:: BeforeRender
diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst
index faca0fbb6..cb411458d 100644
--- a/docs/api/exceptions.rst
+++ b/docs/api/exceptions.rst
@@ -5,6 +5,8 @@
.. automodule:: pyramid.exceptions
+ .. autoexception:: BadCSRFOrigin
+
.. autoexception:: BadCSRFToken
.. autoexception:: PredicateMismatch
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index de2a664a4..272820a91 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -17,6 +17,9 @@ Event-Related Interfaces
.. autointerface:: IContextFound
:members:
+ .. autointerface:: IBeforeTraversal
+ :members:
+
.. autointerface:: INewResponse
:members:
@@ -91,3 +94,9 @@ Other Interfaces
.. autointerface:: ICacheBuster
:members:
+
+ .. autointerface:: IViewDeriver
+ :members:
+
+ .. autointerface:: IViewDeriverInfo
+ :members:
diff --git a/docs/api/paster.rst b/docs/api/paster.rst
index edc3738fc..27bc81a1f 100644
--- a/docs/api/paster.rst
+++ b/docs/api/paster.rst
@@ -11,4 +11,4 @@
.. autofunction:: get_appsettings(config_uri, name=None, options=None)
- .. autofunction:: setup_logging(config_uri)
+ .. autofunction:: setup_logging(config_uri, global_conf=None)
diff --git a/docs/api/request.rst b/docs/api/request.rst
index 105ffb5a7..52bf50078 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -13,7 +13,8 @@
current_route_path, static_url, static_path,
model_url, resource_url, resource_path, set_property,
effective_principals, authenticated_userid,
- unauthenticated_userid, has_permission
+ unauthenticated_userid, has_permission,
+ invoke_exception_view
.. attribute:: context
@@ -259,6 +260,8 @@
See also :ref:`subrequest_chapter`.
+ .. automethod:: invoke_exception_view
+
.. automethod:: has_permission
.. automethod:: add_response_callback
diff --git a/docs/api/session.rst b/docs/api/session.rst
index 474e2bb32..56c4f52d7 100644
--- a/docs/api/session.rst
+++ b/docs/api/session.rst
@@ -9,6 +9,8 @@
.. autofunction:: signed_deserialize
+ .. autofunction:: check_csrf_origin
+
.. autofunction:: check_csrf_token
.. autofunction:: SignedCookieSessionFactory
diff --git a/docs/api/viewderivers.rst b/docs/api/viewderivers.rst
new file mode 100644
index 000000000..2a141501e
--- /dev/null
+++ b/docs/api/viewderivers.rst
@@ -0,0 +1,17 @@
+.. _viewderivers_module:
+
+:mod:`pyramid.viewderivers`
+---------------------------
+
+.. automodule:: pyramid.viewderivers
+
+ .. attribute:: INGRESS
+
+ Constant representing the request ingress, for use in ``under``
+ arguments to :meth:`pyramid.config.Configurator.add_view_deriver`.
+
+ .. attribute:: VIEW
+
+ Constant representing the :term:`view callable` at the end of the view
+ pipeline, for use in ``over`` arguments to
+ :meth:`pyramid.config.Configurator.add_view_deriver`.
diff --git a/docs/conf.py b/docs/conf.py
index a895bc6c3..786ff3abf 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -66,8 +66,8 @@ intersphinx_mapping = {
'deform': ('http://docs.pylonsproject.org/projects/deform/en/latest', None),
'jinja2': ('http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/', None),
'pylonswebframework': ('http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/', None),
- 'python': ('http://docs.python.org', None),
- 'python3': ('http://docs.python.org/3', None),
+ 'python': ('https://docs.python.org/3', None),
+ 'pytest': ('http://pytest.org/latest/', None),
'sqla': ('http://docs.sqlalchemy.org/en/latest', None),
'tm': ('http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/', None),
'toolbar': ('http://docs.pylonsproject.org/projects/pyramid-debugtoolbar/en/latest', None),
@@ -137,6 +137,7 @@ if book:
# 'whatsnew-1.3': 'index',
# 'whatsnew-1.4': 'index',
# 'whatsnew-1.5': 'index',
+# 'whatsnew-1.6': 'index',
# 'tutorials/gae/index': 'index',
# 'api/chameleon_text': 'api',
# 'api/chameleon_zpt': 'api',
@@ -146,9 +147,10 @@ html_theme = 'pyramid'
html_theme_path = pylons_sphinx_themes.get_html_themes_path()
html_theme_options = dict(
github_url='https://github.com/Pylons/pyramid',
- # on master branch true, else false
+ # On master branch and new branch still in
+ # pre-release status: true; else: false.
in_progress='true',
- # on previous branches/major releases true, else false
+ # On branches previous to "latest": true; else: false.
outdated='false',
)
diff --git a/docs/conventions.rst b/docs/conventions.rst
index a9d2550bf..4469d0c73 100644
--- a/docs/conventions.rst
+++ b/docs/conventions.rst
@@ -53,46 +53,46 @@ Code and configuration file blocks are presented in the following style:
Example blocks representing UNIX shell commands are prefixed with a ``$``
character, e.g.:
- .. code-block:: text
+ .. code-block:: bash
- $ $VENV/bin/nosetests
+ $ $VENV/bin/py.test tutorial/tests.py -q
-(See :term:`virtualenv` for the meaning of ``$VENV``)
+(See :term:`venv` for the meaning of ``$VENV``)
Example blocks representing Windows ``cmd.exe`` commands are prefixed with a
drive letter and/or a directory name, e.g.:
- .. code-block:: text
+ .. code-block:: doscon
- c:\examples> %VENV%\Scripts\nosetests
+ c:\examples> %VENV%\Scripts\py.test tutorial\tests.py -q
-(See :term:`virtualenv` for the meaning of ``%VENV%``)
+(See :term:`venv` for the meaning of ``%VENV%``)
Sometimes, when it's unknown which directory is current, Windows ``cmd.exe``
example block commands are prefixed only with a ``>`` character, e.g.:
- .. code-block:: text
+ .. code-block:: doscon
- > %VENV%\Scripts\nosetests
+ > %VENV%\Scripts\py.test tutorial\tests.py -q
When a command that should be typed on one line is too long to fit on a page,
-the backslash ``\`` is used to indicate that the following printed line
-should actually be part of the command:
+the backslash ``\`` is used to indicate that the following printed line should
+be part of the command:
- .. code-block:: text
+ .. code-block:: bash
- c:\bigfntut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \
- --cover-erase --with-coverage
+ $VENV/bin/py.test tutorial/tests.py --cov-report term-missing \
+ --cov=tutorial -q
-A sidebar, which presents a concept tangentially related to content
-discussed on a page, is rendered like so:
+A sidebar, which presents a concept tangentially related to content discussed
+on a page, is rendered like so:
.. sidebar:: This is a sidebar
Sidebar information.
-When multiple objects are imported from the same package,
-the following convention is used:
+When multiple objects are imported from the same package, the following
+convention is used:
.. code-block:: python
@@ -103,9 +103,9 @@ the following convention is used:
It may look unusual, but it has advantages:
-* It allows one to swap out the higher-level package ``foo`` for something
- else that provides the similar API. An example would be swapping out
- one database for another (e.g., graduating from SQLite to PostgreSQL).
+* It allows one to swap out the higher-level package ``foo`` for something else
+ that provides the similar API. An example would be swapping out one database
+ for another (e.g., graduating from SQLite to PostgreSQL).
* Looks more neat in cases where a large number of objects get imported from
that package.
diff --git a/docs/designdefense.rst b/docs/designdefense.rst
index d33ae2fd8..5f3295305 100644
--- a/docs/designdefense.rst
+++ b/docs/designdefense.rst
@@ -1011,8 +1011,8 @@ Self-described "microframeworks" exist. `Bottle <http://bottle.paws.de>`_ and
<http://bobo.digicool.com/>`_ doesn't describe itself as a microframework, but
its intended user base is much the same. Many others exist. We've even (only as
a teaching tool, not as any sort of official project) `created one using
-Pyramid <http://bfg.repoze.org/videos#groundhog1>`_. The videos use BFG, a
-precursor to Pyramid, but the resulting code is `available for Pyramid too
+Pyramid <http://static.repoze.org/casts/videotags.html>`_. The videos use BFG,
+a precursor to Pyramid, but the resulting code is `available for Pyramid too
<https://github.com/Pylons/groundhog>`_). Microframeworks are small frameworks
with one common feature: each allows its users to create a fully functional
application that lives in a single Python file.
@@ -1283,7 +1283,7 @@ predictability.
.. _routes_need_ordering:
-Routes Need Relative Ordering
+Routes need relative ordering
+++++++++++++++++++++++++++++
Consider the following simple `Groundhog
@@ -1311,8 +1311,8 @@ Consider the following simple `Groundhog
app.run()
If you run this application and visit the URL ``/admin``, you will see the
-"admin" page. This is the intended result. However, what if you rearrange
-the order of the function definitions in the file?
+"admin" page. This is the intended result. However, what if you rearrange the
+order of the function definitions in the file?
.. code-block:: python
:linenos:
@@ -1335,11 +1335,11 @@ the order of the function definitions in the file?
if __name__ == '__main__':
app.run()
-If you run this application and visit the URL ``/admin``, you will now be
-returned a 404 error. This is probably not what you intended. The reason
-you see a 404 error when you rearrange function definition ordering is that
-routing declarations expressed via our microframework's routing decorators
-have an *ordering*, and that ordering matters.
+If you run this application and visit the URL ``/admin``, your app will now
+return a 404 error. This is probably not what you intended. The reason you see
+a 404 error when you rearrange function definition ordering is that routing
+declarations expressed via our microframework's routing decorators have an
+*ordering*, and that ordering matters.
In the first case, where we achieved the expected result, we first added a
route with the pattern ``/admin``, then we added a route with the pattern
@@ -1347,65 +1347,67 @@ route with the pattern ``/admin``, then we added a route with the pattern
scope. When a request with a ``PATH_INFO`` of ``/admin`` enters our
application, the web framework loops over each of our application's route
patterns in the order in which they were defined in our module. As a result,
-the view associated with the ``/admin`` routing pattern will be invoked: it
-matches first. All is right with the world.
+the view associated with the ``/admin`` routing pattern will be invoked because
+it matches first. All is right with the world.
In the second case, where we did not achieve the expected result, we first
added a route with the pattern ``/:action``, then we added a route with the
pattern ``/admin``. When a request with a ``PATH_INFO`` of ``/admin`` enters
our application, the web framework loops over each of our application's route
patterns in the order in which they were defined in our module. As a result,
-the view associated with the ``/:action`` routing pattern will be invoked: it
-matches first. A 404 error is raised. This is not what we wanted; it just
-happened due to the order in which we defined our view functions.
-
-This is because Groundhog routes are added to the routing map in import
-order, and matched in the same order when a request comes in. Bottle, like
-Groundhog, as of this writing, matches routes in the order in which they're
-defined at Python execution time. Flask, on the other hand, does not order
-route matching based on import order; it reorders the routes you add to your
-application based on their "complexity". Other microframeworks have varying
+the view associated with the ``/:action`` routing pattern will be invoked
+because it matches first. A 404 error is raised. This is not what we wanted; it
+just happened due to the order in which we defined our view functions.
+
+This is because Groundhog routes are added to the routing map in import order,
+and matched in the same order when a request comes in. Bottle, like Groundhog,
+as of this writing, matches routes in the order in which they're defined at
+Python execution time. Flask, on the other hand, does not order route matching
+based on import order. Instead it reorders the routes you add to your
+application based on their "complexity". Other microframeworks have varying
strategies to do route ordering.
Your application may be small enough where route ordering will never cause an
-issue. If your application becomes large enough, however, being able to
-specify or predict that ordering as your application grows larger will be
-difficult. At some point, you will likely need to more explicitly start
-controlling route ordering, especially in applications that require
-extensibility.
+issue. If your application becomes large enough, however, being able to specify
+or predict that ordering as your application grows larger will be difficult.
+At some point, you will likely need to start controlling route ordering more
+explicitly, especially in applications that require extensibility.
If your microframework orders route matching based on complexity, you'll need
to understand what is meant by "complexity", and you'll need to attempt to
-inject a "less complex" route to have it get matched before any "more
-complex" one to ensure that it's tried first.
+inject a "less complex" route to have it get matched before any "more complex"
+one to ensure that it's tried first.
If your microframework orders its route matching based on relative
import/execution of function decorator definitions, you will need to ensure
-you execute all of these statements in the "right" order, and you'll need to
-be cognizant of this import/execution ordering as you grow your application
-or try to extend it. This is a difficult invariant to maintain for all but
-the smallest applications.
-
-In either case, your application must import the non-``__main__`` modules
-which contain configuration decorations somehow for their configuration to be
-executed. Does that make you a little uncomfortable? It should, because
+that you execute all of these statements in the "right" order, and you'll need
+to be cognizant of this import/execution ordering as you grow your application
+or try to extend it. This is a difficult invariant to maintain for all but the
+smallest applications.
+
+In either case, your application must import the non-``__main__`` modules which
+contain configuration decorations somehow for their configuration to be
+executed. Does that make you a little uncomfortable? It should, because
:ref:`you_dont_own_modulescope`.
Pyramid uses neither decorator import time ordering nor does it attempt to
-divine the relative complexity of one route to another in order to define a
-route match ordering. In Pyramid, you have to maintain relative route
-ordering imperatively via the chronology of multiple executions of the
-:meth:`pyramid.config.Configurator.add_route` method. The order in which you
+divine the relative complexity of one route to another as a means to define a
+route match ordering. In Pyramid, you have to maintain relative route ordering
+imperatively via the chronology of multiple executions of the
+:meth:`pyramid.config.Configurator.add_route` method. The order in which you
repeatedly call ``add_route`` becomes the order of route matching.
If needing to maintain this imperative ordering truly bugs you, you can use
-:term:`traversal` instead of route matching, which is a completely
-declarative (and completely predictable) mechanism to map code to URLs.
-While URL dispatch is easier to understand for small non-extensible
-applications, traversal is a great fit for very large applications and
-applications that need to be arbitrarily extensible.
+:term:`traversal` instead of route matching, which is a completely declarative
+(and completely predictable) mechanism to map code to URLs. While URL dispatch
+is easier to understand for small non-extensible applications, traversal is a
+great fit for very large applications and applications that need to be
+arbitrarily extensible.
-"Stacked Object Proxies" Are Too Clever / Thread Locals Are A Nuisance
+
+.. _thread_local_nuisance:
+
+"Stacked object proxies" are too clever / thread locals are a nuisance
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Some microframeworks use the ``import`` statement to get a handle to an
@@ -1448,32 +1450,35 @@ code below:
for i in range(10):
print(i)
-By its nature, the *request* object created as the result of a WSGI server's
-call into a long-lived web framework cannot be global, because the lifetime
-of a single request will be much shorter than the lifetime of the process
-running the framework. A request object created by a web framework actually
-has more similarity to the ``i`` loop counter in our example above than it
-has to any comparable importable object defined in the Python standard
+By its nature, the *request* object that is created as the result of a WSGI
+server's call into a long-lived web framework cannot be global, because the
+lifetime of a single request will be much shorter than the lifetime of the
+process running the framework. A request object created by a web framework
+actually has more similarity to the ``i`` loop counter in our example above
+than it has to any comparable importable object defined in the Python standard
library or in normal library code.
However, systems which use stacked object proxies promote locally scoped
-objects such as ``request`` out to module scope, for the purpose of being
+objects, such as ``request``, out to module scope, for the purpose of being
able to offer users a nice spelling involving ``import``. They, for what I
-consider dubious reasons, would rather present to their users the canonical
-way of getting at a ``request`` as ``from framework import request`` instead
-of a saner ``from myframework.threadlocals import get_request; request =
-get_request()`` even though the latter is more explicit.
+consider dubious reasons, would rather present to their users the canonical way
+of getting at a ``request`` as ``from framework import request`` instead of a
+saner ``from myframework.threadlocals import get_request; request =
+get_request()``, even though the latter is more explicit.
It would be *most* explicit if the microframeworks did not use thread local
-variables at all. Pyramid view functions are passed a request object; many
-of Pyramid's APIs require that an explicit request object be passed to them.
-It is *possible* to retrieve the current Pyramid request as a threadlocal
-variable but it is a "in case of emergency, break glass" type of activity.
-This explicitness makes Pyramid view functions more easily unit testable, as
-you don't need to rely on the framework to manufacture suitable "dummy"
-request (and other similarly-scoped) objects during test setup. It also
-makes them more likely to work on arbitrary systems, such as async servers
-that do no monkeypatching.
+variables at all. Pyramid view functions are passed a request object. Many of
+Pyramid's APIs require that an explicit request object be passed to them. It is
+*possible* to retrieve the current Pyramid request as a threadlocal variable,
+but it is an "in case of emergency, break glass" type of activity. This
+explicitness makes Pyramid view functions more easily unit testable, as you
+don't need to rely on the framework to manufacture suitable "dummy" request
+(and other similarly-scoped) objects during test setup. It also makes them
+more likely to work on arbitrary systems, such as async servers, that do no
+monkeypatching.
+
+
+.. _explicitly_wsgi:
Explicitly WSGI
+++++++++++++++
@@ -1487,35 +1492,35 @@ import a WSGI server and use it to serve up their Pyramid application as per
the documentation of that WSGI server.
The extra lines saved by abstracting away the serving step behind ``run()``
-seem to have driven dubious second-order decisions related to API in some
-microframeworks. For example, Bottle contains a ``ServerAdapter`` subclass
-for each type of WSGI server it supports via its ``app.run()`` mechanism.
-This means that there exists code in ``bottle.py`` that depends on the
-following modules: ``wsgiref``, ``flup``, ``paste``, ``cherrypy``, ``fapws``,
+seems to have driven dubious second-order decisions related to its API in some
+microframeworks. For example, Bottle contains a ``ServerAdapter`` subclass for
+each type of WSGI server it supports via its ``app.run()`` mechanism. This
+means that there exists code in ``bottle.py`` that depends on the following
+modules: ``wsgiref``, ``flup``, ``paste``, ``cherrypy``, ``fapws``,
``tornado``, ``google.appengine``, ``twisted.web``, ``diesel``, ``gevent``,
-``gunicorn``, ``eventlet``, and ``rocket``. You choose the kind of server
-you want to run by passing its name into the ``run`` method. In theory, this
-sounds great: I can try Bottle out on ``gunicorn`` just by passing in a name!
-However, to fully test Bottle, all of these third-party systems must be
-installed and functional; the Bottle developers must monitor changes to each
-of these packages and make sure their code still interfaces properly with
-them. This expands the packages required for testing greatly; this is a
-*lot* of requirements. It is likely difficult to fully automate these tests
-due to requirements conflicts and build issues.
+``gunicorn``, ``eventlet``, and ``rocket``. You choose the kind of server you
+want to run by passing its name into the ``run`` method. In theory, this sounds
+great: I can try out Bottle on ``gunicorn`` just by passing in a name! However,
+to fully test Bottle, all of these third-party systems must be installed and
+functional. The Bottle developers must monitor changes to each of these
+packages and make sure their code still interfaces properly with them. This
+increases the number of packages required for testing greatly; this is a *lot*
+of requirements. It is likely difficult to fully automate these tests due to
+requirements conflicts and build issues.
As a result, for single-file apps, we currently don't bother to offer a
-``run()`` shortcut; we tell folks to import their WSGI server of choice and
-run it by hand. For the people who want a server abstraction layer, we
-suggest that they use PasteDeploy. In PasteDeploy-based systems, the onus
-for making sure that the server can interface with a WSGI application is
-placed on the server developer, not the web framework developer, making it
-more likely to be timely and correct.
-
-Wrapping Up
+``run()`` shortcut. We tell folks to import their WSGI server of choice and run
+it by hand. For the people who want a server abstraction layer, we suggest that
+they use PasteDeploy. In PasteDeploy-based systems, the onus for making sure
+that the server can interface with a WSGI application is placed on the server
+developer, not the web framework developer, making it more likely to be timely
+and correct.
+
+Wrapping up
+++++++++++
-Here's a diagrammed version of the simplest pyramid application, where
-comments take into account what we've discussed in the
+Here's a diagrammed version of the simplest pyramid application, where the
+inlined comments take into account what we've discussed in the
:ref:`microframeworks_smaller_hello_world` section.
.. code-block:: python
@@ -1526,17 +1531,18 @@ comments take into account what we've discussed in the
def hello_world(request): # accepts a request; no request thread local reqd
# explicit response object means no response threadlocal
- return Response('Hello world!')
+ return Response('Hello world!')
if __name__ == '__main__':
from pyramid.config import Configurator
- config = Configurator() # no global application object.
+ config = Configurator() # no global application object
config.add_view(hello_world) # explicit non-decorator registration
app = config.make_wsgi_app() # explicitly WSGI
server = make_server('0.0.0.0', 8080, app)
server.serve_forever() # explicitly WSGI
-Pyramid Doesn't Offer Pluggable Apps
+
+Pyramid doesn't offer pluggable apps
------------------------------------
It is "Pyramidic" to compose multiple external sources into the same
@@ -1544,7 +1550,7 @@ configuration using :meth:`~pyramid.config.Configurator.include`. Any
number of includes can be done to compose an application; includes can even
be done from within other includes. Any directive can be used within an
include that can be used outside of one (such as
-:meth:`~pyramid.config.Configurator.add_view`, etc).
+:meth:`~pyramid.config.Configurator.add_view`).
Pyramid has a conflict detection system that will throw an error if two
included externals try to add the same configuration in a conflicting way
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 2683ff369..1d97bffe8 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -155,9 +155,9 @@ Glossary
request before it returns a :term:`context` resource.
virtualenv
- A term referring both to an isolated Python environment,
- or `the leading tool <http://www.virtualenv.org>`_ that allows one to
- create such environments.
+ The `virtualenv tool <https://virtualenv.pypa.io/en/latest/>`_ that allows
+ one to create virtual environments. In Python 3.3 and greater,
+ :term:`venv` is the preferred tool.
Note: whenever you encounter commands prefixed with ``$VENV`` (Unix)
or ``%VENV`` (Windows), know that that is the environment variable whose
@@ -367,13 +367,13 @@ Glossary
file. It was developed by Ian Bicking.
Chameleon
- `chameleon <http://chameleon.repoze.org>`_ is an attribute language
- template compiler which supports the :term:`ZPT` templating
- specification. It is written and maintained by Malthe Borch. It has
- several extensions, such as the ability to use bracketed (Mako-style)
- ``${name}`` syntax. It is also much faster than the reference
- implementation of ZPT. :app:`Pyramid` offers Chameleon templating out
- of the box in ZPT and text flavors.
+ `chameleon <https://chameleon.readthedocs.org/en/latest/>`_ is an
+ attribute language template compiler which supports the :term:`ZPT`
+ templating specification. It is written and maintained by Malthe Borch. It
+ has several extensions, such as the ability to use bracketed (Mako-style)
+ ``${name}`` syntax. It is also much faster than the reference
+ implementation of ZPT. :app:`Pyramid` offers Chameleon templating out of
+ the box in ZPT and text flavors.
ZPT
The `Zope Page Template <http://wiki.zope.org/ZPT/FrontPage>`_
@@ -558,12 +558,11 @@ Glossary
A popular `Javascript library <http://jquery.org>`_.
renderer
- A serializer that can be referred to via :term:`view
- configuration` which converts a non-:term:`Response` return
- values from a :term:`view` into a string (and ultimately a
- response). Using a renderer can make writing views that require
- templating or other serialization less tedious. See
- :ref:`views_which_use_a_renderer` for more information.
+ A serializer which converts non-:term:`Response` return values from a
+ :term:`view` into a string, and ultimately into a response, usually
+ through :term:`view configuration`. Using a renderer can make writing
+ views that require templating or other serialization, like JSON, less
+ tedious. See :ref:`views_which_use_a_renderer` for more information.
renderer factory
A factory which creates a :term:`renderer`. See
@@ -815,11 +814,10 @@ Glossary
library, used by the :app:`Pyramid` translation machinery.
Babel
- A `collection of tools <http://babel.edgewall.org/>`_ for
- internationalizing Python applications. :app:`Pyramid` does
- not depend on Babel to operate, but if Babel is installed,
- additional locale functionality becomes available to your
- application.
+ A `collection of tools <http://babel.pocoo.org/en/latest/>`_ for
+ internationalizing Python applications. :app:`Pyramid` does not depend on
+ Babel to operate, but if Babel is installed, additional locale
+ functionality becomes available to your application.
Lingua
A package by Wichert Akkerman which provides the ``pot-create``
@@ -1014,8 +1012,8 @@ Glossary
console script
A script written to the ``bin`` (on UNIX, or ``Scripts`` on Windows)
- directory of a Python installation or :term:`virtualenv` as the result of
- running ``setup.py install`` or ``setup.py develop``.
+ directory of a Python installation or :term:`virtual environment` as the
+ result of running ``pip install`` or ``pip install -e .``.
introspector
An object with the methods described by
@@ -1093,3 +1091,48 @@ Glossary
A technique used when serving a cacheable static asset in order to force
a client to query the new version of the asset. See :ref:`cache_busting`
for more information.
+
+ view deriver
+ A view deriver is a composable component of the view pipeline which is
+ used to create a :term:`view callable`. A view deriver is a callable
+ implementing the :class:`pyramid.interfaces.IViewDeriver` interface.
+ Examples of built-in derivers including view mapper, the permission
+ checker, and applying a renderer to a dictionary returned from the view.
+
+ truthy string
+ A string represeting a value of ``True``. Acceptable values are
+ ``t``, ``true``, ``y``, ``yes``, ``on`` and ``1``.
+
+ falsey string
+ A string represeting a value of ``False``. Acceptable values are
+ ``f``, ``false``, ``n``, ``no``, ``off`` and ``0``.
+
+ pip
+ The :term:`Python Packaging Authority`'s recommended tool for installing
+ Python packages.
+
+ pyvenv
+ The :term:`Python Packaging Authority` formerly recommended using the
+ ``pyvenv`` command for `creating virtual environments on Python 3.4 and
+ 3.5
+ <https://packaging.python.org/en/latest/installing/#creating-virtual-environments>`_,
+ but it was deprecated in 3.6 in favor of ``python3 -m venv`` on UNIX or
+ ``python -m venv`` on Windows, which is backward compatible on Python
+ 3.3 and greater.
+
+ virtual environment
+ An isolated Python environment that allows packages to be installed for
+ use by a particular application, rather than being installed system wide.
+
+ venv
+ The :term:`Python Packaging Authority`'s recommended tool for creating
+ virtual environments on Python 3.3 and greater.
+
+ Note: whenever you encounter commands prefixed with ``$VENV`` (Unix)
+ or ``%VENV`` (Windows), know that that is the environment variable whose
+ value is the root of the virtual environment in question.
+
+ Python Packaging Authority
+ The `Python Packaging Authority (PyPA) <https://www.pypa.io/en/latest/>`_
+ is a working group that maintains many of the relevant projects in Python
+ packaging. \ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index ba6ca1e49..aecc26d2e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -181,6 +181,7 @@ Change History
.. toctree::
:maxdepth: 1
+ whatsnew-1.7
whatsnew-1.6
whatsnew-1.5
whatsnew-1.4
diff --git a/docs/narr/MyProject/README.txt b/docs/narr/MyProject/README.txt
index c28d0d94a..70759eba1 100644
--- a/docs/narr/MyProject/README.txt
+++ b/docs/narr/MyProject/README.txt
@@ -1 +1,12 @@
MyProject README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
+
diff --git a/docs/narr/MyProject/development.ini b/docs/narr/MyProject/development.ini
index 749e574eb..94fece8ce 100644
--- a/docs/narr/MyProject/development.ini
+++ b/docs/narr/MyProject/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,7 +29,7 @@ port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
diff --git a/docs/narr/MyProject/myproject/static/theme.min.css b/docs/narr/MyProject/myproject/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/narr/MyProject/myproject/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/narr/MyProject/myproject/templates/mytemplate.pt b/docs/narr/MyProject/myproject/templates/mytemplate.pt
index 65d7f0609..543663fe8 100644
--- a/docs/narr/MyProject/myproject/templates/mytemplate.pt
+++ b/docs/narr/MyProject/myproject/templates/mytemplate.pt
@@ -34,15 +34,15 @@
<div class="col-md-10">
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.6b2</span>.</p>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
</div>
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li class="current-version">Generated by v1.6b2</li>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.6-branch/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/narr/MyProject/myproject/tests.py b/docs/narr/MyProject/myproject/tests.py
index 37df08a2a..fd414cced 100644
--- a/docs/narr/MyProject/myproject/tests.py
+++ b/docs/narr/MyProject/myproject/tests.py
@@ -26,4 +26,4 @@ class FunctionalTests(unittest.TestCase):
def test_root(self):
res = self.testapp.get('/', status=200)
- self.assertTrue('Pyramid' in res.body)
+ self.assertTrue(b'Pyramid' in res.body)
diff --git a/docs/narr/MyProject/production.ini b/docs/narr/MyProject/production.ini
index 3ccbe6619..1174b1cc7 100644
--- a/docs/narr/MyProject/production.ini
+++ b/docs/narr/MyProject/production.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -23,7 +23,7 @@ port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
diff --git a/docs/narr/MyProject/setup.py b/docs/narr/MyProject/setup.py
index 8c019af51..a911eff6d 100644
--- a/docs/narr/MyProject/setup.py
+++ b/docs/narr/MyProject/setup.py
@@ -15,16 +15,22 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='MyProject',
version='0.0',
description='MyProject',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -32,9 +38,10 @@ setup(name='MyProject',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
- tests_require=requires,
- test_suite="myproject",
entry_points="""\
[paste.app_factory]
main = myproject:main
diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst
index 34b12e1e9..6cd90d42f 100644
--- a/docs/narr/commandline.rst
+++ b/docs/narr/commandline.rst
@@ -119,7 +119,7 @@ The Interactive Shell
.. seealso:: See also the output of :ref:`pshell --help <pshell_script>`.
-Once you've installed your program for development using ``setup.py develop``,
+Once you've installed your program for development using ``pip install -e .``,
you can use an interactive Python shell to execute expressions in a Python
environment exactly like the one that will be used when your application runs
"for real". To do so, use the ``pshell`` command line utility.
@@ -294,7 +294,7 @@ You may use the ``--list-shells`` option to see the available shells.
python
If you want to use a shell that isn't supported out of the box, you can
-introduce a new shell by registering an entry point in your setup.py:
+introduce a new shell by registering an entry point in your ``setup.py``:
.. code-block:: python
@@ -578,10 +578,10 @@ Using Custom Arguments to Python when Running ``p*`` Scripts
.. versionadded:: 1.5
Each of Pyramid's console scripts (``pserve``, ``pviews``, etc.) can be run
-directly using ``python -m``, allowing custom arguments to be sent to the
+directly using ``python3 -m``, allowing custom arguments to be sent to the
Python interpreter at runtime. For example::
- python -3 -m pyramid.scripts.pserve development.ini
+ python3 -m pyramid.scripts.pserve development.ini
.. index::
@@ -815,17 +815,17 @@ Making Your Script into a Console Script
----------------------------------------
A "console script" is :term:`setuptools` terminology for a script that gets
-installed into the ``bin`` directory of a Python :term:`virtualenv` (or "base"
-Python environment) when a :term:`distribution` which houses that script is
-installed. Because it's installed into the ``bin`` directory of a virtualenv
-when the distribution is installed, it's a convenient way to package and
-distribute functionality that you can call from the command-line. It's often
-more convenient to create a console script than it is to create a ``.py``
-script and instruct people to call it with the "right" Python interpreter. A
-console script generates a file that lives in ``bin``, and when it's invoked it
-will always use the "right" Python environment, which means it will always be
-invoked in an environment where all the libraries it needs (such as Pyramid)
-are available.
+installed into the ``bin`` directory of a Python :term:`virtual environment`
+(or "base" Python environment) when a :term:`distribution` which houses that
+script is installed. Because it's installed into the ``bin`` directory of a
+virtual environment when the distribution is installed, it's a convenient way
+to package and distribute functionality that you can call from the
+command-line. It's often more convenient to create a console script than it is
+to create a ``.py`` script and instruct people to call it with the "right"
+Python interpreter. A console script generates a file that lives in ``bin``,
+and when it's invoked it will always use the "right" Python environment, which
+means it will always be invoked in an environment where all the libraries it
+needs (such as Pyramid) are available.
In general, you can make your script into a console script by doing the
following:
@@ -840,12 +840,11 @@ following:
distribution which creates a mapping between a script name and a dotted name
representing the callable you added to your distribution.
-- Run ``setup.py develop``, ``setup.py install``, or ``easy_install`` to get
- your distribution reinstalled. When you reinstall your distribution, a file
- representing the script that you named in the last step will be in the
- ``bin`` directory of the virtualenv in which you installed the distribution.
- It will be executable. Invoking it from a terminal will execute your
- callable.
+- Run ``pip install -e .`` or ``pip install .`` to get your distribution
+ reinstalled. When you reinstall your distribution, a file representing the
+ script that you named in the last step will be in the ``bin`` directory of
+ the virtual environment in which you installed the distribution. It will be
+ executable. Invoking it from a terminal will execute your callable.
As an example, let's create some code that can be invoked by a console script
that prints the deployment settings of a Pyramid application. To do so, we'll
@@ -927,16 +926,22 @@ top-level directory, your ``setup.py`` file will look something like this:
requires = ['pyramid', 'pyramid_debugtoolbar']
+ tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='MyProject',
version='0.0',
description='My project',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pylons",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -945,20 +950,23 @@ top-level directory, your ``setup.py`` file will look something like this:
include_package_data=True,
zip_safe=False,
install_requires=requires,
- tests_require=requires,
- test_suite="myproject",
+ extras_require={
+ 'testing': tests_require,
+ },
entry_points = """\
[paste.app_factory]
main = myproject:main
""",
)
-We're going to change the setup.py file to add a ``[console_scripts]`` section
-within the ``entry_points`` string. Within this section, you should specify a
-``scriptname = dotted.path.to:yourfunction`` line. For example::
+We're going to change the ``setup.py`` file to add a ``[console_scripts]``
+section within the ``entry_points`` string. Within this section, you should
+specify a ``scriptname = dotted.path.to:yourfunction`` line. For example:
+
+.. code-block:: ini
- [console_scripts]
- show_settings = myproject.scripts:settings_show
+ [console_scripts]
+ show_settings = myproject.scripts:settings_show
The ``show_settings`` name will be the name of the script that is installed
into ``bin``. The colon (``:``) between ``myproject.scripts`` and
@@ -971,6 +979,7 @@ The result will be something like:
.. code-block:: python
:linenos:
+ :emphasize-lines: 43-44
import os
@@ -984,16 +993,22 @@ The result will be something like:
requires = ['pyramid', 'pyramid_debugtoolbar']
+ tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='MyProject',
version='0.0',
description='My project',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pylons",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -1002,8 +1017,9 @@ The result will be something like:
include_package_data=True,
zip_safe=False,
install_requires=requires,
- tests_require=requires,
- test_suite="myproject",
+ extras_require={
+ 'testing': tests_require,
+ },
entry_points = """\
[paste.app_factory]
main = myproject:main
@@ -1012,15 +1028,17 @@ The result will be something like:
""",
)
-Once you've done this, invoking ``$$VENV/bin/python setup.py develop`` will
-install a file named ``show_settings`` into the ``$somevirtualenv/bin``
-directory with a small bit of Python code that points to your entry point. It
-will be executable. Running it without any arguments will print an error and
-exit. Running it with a single argument that is the path of a config file will
-print the settings. Running it with an ``--omit=foo`` argument will omit the
-settings that have keys that start with ``foo``. Running it with two "omit"
-options (e.g., ``--omit=foo --omit=bar``) will omit all settings that have keys
-that start with either ``foo`` or ``bar``::
+Once you've done this, invoking ``$VENV/bin/pip install -e .`` will install a
+file named ``show_settings`` into the ``$somevenv/bin`` directory with a
+small bit of Python code that points to your entry point. It will be
+executable. Running it without any arguments will print an error and exit.
+Running it with a single argument that is the path of a config file will print
+the settings. Running it with an ``--omit=foo`` argument will omit the settings
+that have keys that start with ``foo``. Running it with two "omit" options
+(e.g., ``--omit=foo --omit=bar``) will omit all settings that have keys that
+start with either ``foo`` or ``bar``:
+
+.. code-block:: bash
$ $VENV/bin/show_settings development.ini --omit=pyramid --omit=debugtoolbar
debug_routematch False
diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst
index fee8d0d3a..af7d0a349 100644
--- a/docs/narr/extconfig.rst
+++ b/docs/narr/extconfig.rst
@@ -259,6 +259,7 @@ Pre-defined Phases
- :meth:`pyramid.config.Configurator.add_route_predicate`
- :meth:`pyramid.config.Configurator.add_subscriber_predicate`
- :meth:`pyramid.config.Configurator.add_view_predicate`
+- :meth:`pyramid.config.Configurator.add_view_deriver`
- :meth:`pyramid.config.Configurator.set_authorization_policy`
- :meth:`pyramid.config.Configurator.set_default_permission`
- :meth:`pyramid.config.Configurator.set_view_mapper`
diff --git a/docs/narr/extending.rst b/docs/narr/extending.rst
index d28eb341d..9dc042024 100644
--- a/docs/narr/extending.rst
+++ b/docs/narr/extending.rst
@@ -197,8 +197,8 @@ this:
elements, such as templates and static assets as necessary.
- Install the new package into the same Python environment as the original
- application (e.g., ``$VENV/bin/python setup.py develop`` or
- ``$VENV/bin/python setup.py install``).
+ application (e.g., ``$VENV/bin/pip install -e .`` or ``$VENV/bin/pip install
+ .``).
- Change the ``main`` function in the new package's ``__init__.py`` to include
the original :app:`Pyramid` application's configuration functions via
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 7ff119b53..28d1e09d5 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -1547,3 +1547,156 @@ in every subscriber registration. It is not the responsibility of the
predicate author to make every predicate make sense for every event type; it is
the responsibility of the predicate consumer to use predicates that make sense
for a particular event type registration.
+
+
+.. index::
+ single: view derivers
+
+.. _view_derivers:
+
+View Derivers
+-------------
+
+.. versionadded:: 1.7
+
+Every URL processed by :app:`Pyramid` is matched against a custom view
+pipeline. See :ref:`router_chapter` for how this works. The view pipeline
+itself is built from the user-supplied :term:`view callable`, which is then
+composed with :term:`view derivers <view deriver>`. A view deriver is a
+composable element of the view pipeline which is used to wrap a view with
+added functionality. View derivers are very similar to the ``decorator``
+argument to :meth:`pyramid.config.Configurator.add_view`, except that they have
+the option to execute for every view in the application.
+
+It is helpful to think of a :term:`view deriver` as middleware for views.
+Unlike tweens or WSGI middleware which are scoped to the application itself,
+a view deriver is invoked once per view in the application, and can use
+configuration options from the view to customize its behavior.
+
+Built-in View Derivers
+~~~~~~~~~~~~~~~~~~~~~~
+
+There are several built-in view derivers that :app:`Pyramid` will automatically
+apply to any view. Below they are defined in order from furthest to closest to
+the user-defined :term:`view callable`:
+
+``secured_view``
+
+ Enforce the ``permission`` defined on the view. This element is a no-op if no
+ permission is defined. Note there will always be a permission defined if a
+ default permission was assigned via
+ :meth:`pyramid.config.Configurator.set_default_permission`.
+
+ This element will also output useful debugging information when
+ ``pyramid.debug_authorization`` is enabled.
+
+``csrf_view``
+
+ Used to check the CSRF token provided in the request. This element is a
+ no-op if both the ``require_csrf`` view option and the
+ ``pyramid.require_default_csrf`` setting are disabled.
+
+``owrapped_view``
+
+ Invokes the wrapped view defined by the ``wrapper`` option.
+
+``http_cached_view``
+
+ Applies cache control headers to the response defined by the ``http_cache``
+ option. This element is a no-op if the ``pyramid.prevent_http_cache`` setting
+ is enabled or the ``http_cache`` option is ``None``.
+
+``decorated_view``
+
+ Wraps the view with the decorators from the ``decorator`` option.
+
+``rendered_view``
+
+ Adapts the result of the :term:`view callable` into a :term:`response`
+ object. Below this point the result may be any Python object.
+
+``mapped_view``
+
+ Applies the :term:`view mapper` defined by the ``mapper`` option or the
+ application's default view mapper to the :term:`view callable`. This
+ is always the closest deriver to the user-defined view and standardizes the
+ view pipeline interface to accept ``(context, request)`` from all previous
+ view derivers.
+
+.. warning::
+
+ Any view derivers defined ``under`` the ``rendered_view`` are not
+ guaranteed to receive a valid response object. Rather they will receive the
+ result from the :term:`view mapper` which is likely the original response
+ returned from the view. This is possibly a dictionary for a renderer but it
+ may be any Python object that may be adapted into a response.
+
+Custom View Derivers
+~~~~~~~~~~~~~~~~~~~~
+
+It is possible to define custom view derivers which will affect all views in an
+application. There are many uses for this, but most will likely be centered
+around monitoring and security. In order to register a custom :term:`view
+deriver`, you should create a callable that conforms to the
+:class:`pyramid.interfaces.IViewDeriver` interface, and then register it with
+your application using :meth:`pyramid.config.Configurator.add_view_deriver`.
+For example, below is a callable that can provide timing information for the
+view pipeline:
+
+.. code-block:: python
+ :linenos:
+
+ import time
+
+ def timing_view(view, info):
+ def wrapper_view(context, request):
+ start = time.time()
+ response = view(context, request)
+ end = time.time()
+ response.headers['X-View-Performance'] = '%.3f' % (end - start,)
+ return wrapper_view
+
+ config.add_view_deriver(timing_view)
+
+View derivers are unique in that they have access to most of the options
+passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what
+to do, and they have a chance to affect every view in the application.
+
+Ordering View Derivers
+~~~~~~~~~~~~~~~~~~~~~~
+
+By default, every new view deriver is added between the ``decorated_view`` and
+``rendered_view`` built-in derivers. It is possible to customize this ordering
+using the ``over`` and ``under`` options. Each option can use the names of
+other view derivers in order to specify an ordering. There should rarely be a
+reason to worry about the ordering of the derivers except when the deriver
+depends on other operations in the view pipeline.
+
+Both ``over`` and ``under`` may also be iterables of constraints. For either
+option, if one or more constraints was defined, at least one must be satisfied,
+else a :class:`pyramid.exceptions.ConfigurationError` will be raised. This may
+be used to define fallback constraints if another deriver is missing.
+
+Two sentinel values exist, :attr:`pyramid.viewderivers.INGRESS` and
+:attr:`pyramid.viewderivers.VIEW`, which may be used when specifying
+constraints at the edges of the view pipeline. For example, to add a deriver
+at the start of the pipeline you may use ``under=INGRESS``.
+
+It is not possible to add a view deriver under the ``mapped_view`` as the
+:term:`view mapper` is intimately tied to the signature of the user-defined
+:term:`view callable`. If you simply need to know what the original view
+callable was, it can be found as ``info.original_view`` on the provided
+:class:`pyramid.interfaces.IViewDeriverInfo` object passed to every view
+deriver.
+
+.. warning::
+
+ The default constraints for any view deriver are ``over='rendered_view'``
+ and ``under='decorated_view'``. When escaping these constraints you must
+ take care to avoid cyclic dependencies between derivers. For example, if
+ you want to add a new view deriver before ``secured_view`` then
+ simply specifying ``over='secured_view'`` is not enough, because the
+ default is also under ``decorated view`` there will be an unsatisfiable
+ cycle. You must specify a valid ``under`` constraint as well, such as
+ ``under=INGRESS`` to fall between INGRESS and ``secured_view`` at the
+ beginning of the view pipeline.
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
index ecc48aa2b..014f314ad 100644
--- a/docs/narr/i18n.rst
+++ b/docs/narr/i18n.rst
@@ -265,18 +265,18 @@ available you can install it through the packaging system from your OS; the
package name is almost always ``gettext``. For example on a Debian or Ubuntu
system run this command:
-.. code-block:: text
+.. code-block:: bash
$ sudo apt-get install gettext
Installing Lingua is done with the Python packaging tools. If the
-:term:`virtualenv` into which you've installed your :app:`Pyramid` application
-lives in ``/my/virtualenv``, you can install Lingua like so:
+:term:`virtual environment` into which you've installed your :app:`Pyramid`
+application lives at the environment variable ``$VENV``, you can install Lingua
+like so:
-.. code-block:: text
+.. code-block:: bash
- $ cd /my/virtualenv
- $ $VENV/bin/easy_install lingua
+ $ $VENV/bin/pip install lingua
Installation on Windows
+++++++++++++++++++++++
@@ -288,12 +288,13 @@ compile it yourself. Make sure the installation path is added to your
``$PATH``.
Installing Lingua is done with the Python packaging tools. If the
-:term:`virtualenv` into which you've installed your :app:`Pyramid` application
-lives in ``C:\my\virtualenv``, you can install Lingua like so:
+:term:`virtual environment` into which you've installed your :app:`Pyramid`
+application lives at the environment variable ``%VENV%``, you can install
+Lingua like so:
-.. code-block:: text
+.. code-block:: doscon
- C> %VENV%\Scripts\easy_install lingua
+ C> %VENV%\Scripts\pip install lingua
.. index::
@@ -308,9 +309,9 @@ Once Lingua is installed, you may extract a message catalog template from the
code and :term:`Chameleon` templates which reside in your :app:`Pyramid`
application. You run a ``pot-create`` command to extract the messages:
-.. code-block:: text
+.. code-block:: bash
- $ cd /place/where/myapplication/setup.py/lives
+ $ cd /file/path/to/myapplication_setup.py
$ mkdir -p myapplication/locale
$ $VENV/bin/pot-create -o myapplication/locale/myapplication.pot src
@@ -331,9 +332,9 @@ represents translations of a particular set of messages to a particular locale.
Initialize a ``.po`` file for a specific locale from a pre-generated ``.pot``
template by using the ``msginit`` command from Gettext:
-.. code-block:: text
+.. code-block:: bash
- $ cd /place/where/myapplication/setup.py/lives
+ $ cd /file/path/to/myapplication_setup.py
$ cd myapplication/locale
$ mkdir -p es/LC_MESSAGES
$ msginit -l es -o es/LC_MESSAGES/myapplication.po
@@ -362,9 +363,9 @@ translated or re-translated.
First, regenerate the ``.pot`` file as per :ref:`extracting_messages`. Then use
the ``msgmerge`` command from Gettext.
-.. code-block:: text
+.. code-block:: bash
- $ cd /place/where/myapplication/setup.py/lives
+ $ cd /file/path/to/myapplication_setup.py
$ cd myapplication/locale
$ msgmerge --update es/LC_MESSAGES/myapplication.po myapplication.pot
@@ -380,9 +381,9 @@ Finally, to prepare an application for performing actual runtime translations,
compile ``.po`` files to ``.mo`` files using the ``msgfmt`` command from
Gettext:
-.. code-block:: text
+.. code-block:: bash
- $ cd /place/where/myapplication/setup.py/lives
+ $ cd /file/path/to/myapplication_setup.py
$ msgfmt -o myapplication/locale/es/LC_MESSAGES/myapplication.mo \
myapplication/locale/es/LC_MESSAGES/myapplication.po
@@ -585,10 +586,10 @@ Performing Date Formatting and Currency Formatting
:app:`Pyramid` does not itself perform date and currency formatting for
different locales. However, :term:`Babel` can help you do this via the
:class:`babel.core.Locale` class. The `Babel documentation for this class
-<http://babel.edgewall.org/wiki/ApiDocs/babel.core#babel.core:Locale>`_
-provides minimal information about how to perform date and currency related
-locale operations. See :ref:`installing_babel` for information about how to
-install Babel.
+<http://babel.pocoo.org/en/latest/api/core.html#basic-interface>`_ provides
+minimal information about how to perform date and currency related locale
+operations. See :ref:`installing_babel` for information about how to install
+Babel.
The :class:`babel.core.Locale` class requires a :term:`locale name` as an
argument to its constructor. You can use :app:`Pyramid` APIs to obtain the
@@ -670,6 +671,21 @@ There exists a recipe within the :term:`Pyramid Community Cookbook` named
:ref:`Mako Internationalization <cookbook:mako_i18n>` which explains how to add
idiomatic i18n support to :term:`Mako` templates.
+
+.. index::
+ single: Jinja2 i18n
+
+Jinja2 Pyramid i18n Support
+---------------------------
+
+The add-on `pyramid_jinja2 <https://github.com/Pylons/pyramid_jinja2>`_
+provides a scaffold with an example of how to use internationalization with
+Jinja2 in Pyramid. See the documentation sections `Internalization (i18n)
+<http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/#internalization-i18n>`_
+and `Paster Template I18N
+<http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/#paster-template-i18n>`_.
+
+
.. index::
single: localization deployment settings
single: default_locale_name
diff --git a/docs/narr/install.rst b/docs/narr/install.rst
index 767b16fc0..3e5523262 100644
--- a/docs/narr/install.rst
+++ b/docs/narr/install.rst
@@ -3,38 +3,54 @@
Installing :app:`Pyramid`
=========================
+.. note::
+
+ This installation guide emphasizes the use of Python 3.4 and greater for
+ simplicity.
+
+
.. index::
single: install preparation
-Before You Install
-------------------
+Before You Install Pyramid
+--------------------------
-You will need `Python <http://python.org>`_ version 2.6 or better to run
-:app:`Pyramid`.
+Install Python version 3.4 or greater for your operating system, and satisfy
+the :ref:`requirements-for-installing-packages`, as described in
+the following sections.
.. sidebar:: Python Versions
- As of this writing, :app:`Pyramid` has been tested under Python 2.6, Python
- 2.7, Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3. :app:`Pyramid`
- does not run under any version of Python before 2.6.
+ As of this writing, :app:`Pyramid` has been tested under Python 2.7,
+ Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3. :app:`Pyramid` does
+ not run under any version of Python before 2.7.
:app:`Pyramid` is known to run on all popular UNIX-like systems such as Linux,
-Mac OS X, and FreeBSD as well as on Windows platforms. It is also known to run
-on :term:`PyPy` (1.9+).
+Mac OS X, and FreeBSD, as well as on Windows platforms. It is also known to
+run on :term:`PyPy` (1.9+).
-:app:`Pyramid` installation does not require the compilation of any C code, so
-you need only a Python interpreter that meets the requirements mentioned.
+:app:`Pyramid` installation does not require the compilation of any C code.
+However, some :app:`Pyramid` dependencies may attempt to build binary
+extensions from C code for performance speed ups. If a compiler or Python
+headers are unavailable, the dependency will fall back to using pure Python
+instead.
-Some :app:`Pyramid` dependencies may attempt to build C extensions for
-performance speedups. If a compiler or Python headers are unavailable the
-dependency will fall back to using pure Python instead.
+.. note::
+
+ If you see any warnings or errors related to failing to compile the binary
+ extensions, in most cases you may safely ignore those errors. If you wish to
+ use the binary extensions, please verify that you have a functioning
+ compiler and the Python header files installed for your operating system.
+
+
+.. _for-mac-os-x-users:
For Mac OS X Users
~~~~~~~~~~~~~~~~~~
Python comes pre-installed on Mac OS X, but due to Apple's release cycle, it is
often out of date. Unless you have a need for a specific earlier version, it is
-recommended to install the latest 2.x or 3.x version of Python.
+recommended to install the latest 3.x version of Python.
You can install the latest verion of Python for Mac OS X from the binaries on
`python.org <https://www.python.org/downloads/mac-osx/>`_.
@@ -43,15 +59,15 @@ Alternatively, you can use the `homebrew <http://brew.sh/>`_ package manager.
.. code-block:: text
- # for python 2.7
- $ brew install python
-
- # for python 3.5
+ # for python 3.x
$ brew install python3
If you use an installer for your Python, then you can skip to the section
:ref:`installing_unix`.
+
+.. _if-you-don-t-yet-have-a-python-interpreter-unix:
+
If You Don't Yet Have a Python Interpreter (UNIX)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -60,250 +76,101 @@ either install Python using your operating system's package manager *or* you
can install Python from source fairly easily on any UNIX system that has
development tools.
-.. index::
- pair: install; Python (from package, UNIX)
-
-Package Manager Method
-++++++++++++++++++++++
-
-You can use your system's "package manager" to install Python. Each package
-manager is slightly different, but the "flavor" of them is usually the same.
-
-For example, on a Debian or Ubuntu system, use the following command:
-
-.. code-block:: text
-
- $ sudo apt-get install python2.7-dev
-
-This command will install both the Python interpreter and its development
-header files. Note that the headers are required by some (optional) C
-extensions in software depended upon by Pyramid, not by Pyramid itself.
+.. seealso:: See the official Python documentation :ref:`Using Python on Unix
+ platforms <python:using-on-unix>` for full details.
-Once these steps are performed, the Python interpreter will usually be
-invokable via ``python2.7`` from a shell prompt.
-
-.. index::
- pair: install; Python (from source, UNIX)
-
-Source Compile Method
-+++++++++++++++++++++
-
-It's useful to use a Python interpreter that *isn't* the "system" Python
-interpreter to develop your software. The authors of :app:`Pyramid` tend not
-to use the system Python for development purposes; always a self-compiled one.
-Compiling Python is usually easy, and often the "system" Python is compiled
-with options that aren't optimal for web development. For an explanation, see
-https://github.com/Pylons/pyramid/issues/747.
-
-To compile software on your UNIX system, typically you need development tools.
-Often these can be installed via the package manager. For example, this works
-to do so on an Ubuntu Linux system:
-
-.. code-block:: text
-
- $ sudo apt-get install build-essential
-
-On Mac OS X, installing `XCode <http://developer.apple.com/tools/xcode/>`_ has
-much the same effect.
-
-Once you've got development tools installed on your system, you can install a
-Python 2.7 interpreter from *source*, on the same system, using the following
-commands:
-
-.. code-block:: text
-
- $ cd ~
- $ mkdir tmp
- $ mkdir opt
- $ cd tmp
- $ wget http://www.python.org/ftp/python/2.7.3/Python-2.7.3.tgz
- $ tar xvzf Python-2.7.3.tgz
- $ cd Python-2.7.3
- $ ./configure --prefix=$HOME/opt/Python-2.7.3
- $ make && make install
-
-Once these steps are performed, the Python interpreter will be invokable via
-``$HOME/opt/Python-2.7.3/bin/python`` from a shell prompt.
.. index::
pair: install; Python (from package, Windows)
+.. _if-you-don-t-yet-have-a-python-interpreter-windows:
+
If You Don't Yet Have a Python Interpreter (Windows)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If your Windows system doesn't have a Python interpreter, you'll need to
-install it by downloading a Python 2.7-series interpreter executable from
+install it by downloading a Python 3.x-series interpreter executable from
`python.org's download section <http://python.org/download/>`_ (the files
labeled "Windows Installer"). Once you've downloaded it, double click on the
executable and accept the defaults during the installation process. You may
also need to download and install the Python for Windows extensions.
-.. warning::
-
- After you install Python on Windows, you may need to add the ``C:\Python27``
- directory to your environment's ``Path`` in order to make it possible to
- invoke Python from a command prompt by typing ``python``. To do so, right
- click ``My Computer``, select ``Properties`` --> ``Advanced Tab`` -->
- ``Environment Variables`` and add that directory to the end of the ``Path``
- environment variable.
-
-.. index::
- single: installing on UNIX
-
-.. _installing_unix:
-
-Installing :app:`Pyramid` on a UNIX System
-------------------------------------------
-
-It is best practice to install :app:`Pyramid` into a "virtual" Python
-environment in order to obtain isolation from any "system" packages you've got
-installed in your Python version. This can be done by using the
-:term:`virtualenv` package. Using a virtualenv will also prevent
-:app:`Pyramid` from globally installing versions of packages that are not
-compatible with your system Python.
-
-To set up a virtualenv in which to install :app:`Pyramid`, first ensure that
-:term:`setuptools` is installed. To do so, invoke ``import setuptools`` within
-the Python interpreter you'd like to run :app:`Pyramid` under.
-
-The following command will not display anything if setuptools is already
-installed:
-
-.. code-block:: text
-
- $ python2.7 -c 'import setuptools'
+.. seealso:: See the official Python documentation :ref:`Using Python on
+ Windows <python:using-on-windows>` for full details.
-Running the same command will yield the following output if setuptools is not
-yet installed:
-
-.. code-block:: text
-
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- ImportError: No module named setuptools
-
-If ``import setuptools`` raises an :exc:`ImportError` as it does above, you
-will need to install setuptools manually.
-
-If you are using a "system" Python (one installed by your OS distributor or a
-third-party packager such as Fink or MacPorts), you can usually install the
-setuptools package by using your system's package manager. If you cannot do
-this, or if you're using a self-installed version of Python, you will need to
-install setuptools "by hand". Installing setuptools "by hand" is always a
-reasonable thing to do, even if your package manager already has a pre-chewed
-version of setuptools for installation.
-
-Installing Setuptools
-~~~~~~~~~~~~~~~~~~~~~
-
-To install setuptools by hand under Python 2, first download `ez_setup.py
-<https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py>`_ then invoke
-it using the Python interpreter into which you want to install setuptools.
-
-.. code-block:: text
+.. seealso:: Download and install the `Python for Windows extensions
+ <http://sourceforge.net/projects/pywin32/files/pywin32/>`_. Carefully read
+ the README.txt file at the end of the list of builds, and follow its
+ directions. Make sure you get the proper 32- or 64-bit build and Python
+ version.
- $ python ez_setup.py
+.. warning::
-Once this command is invoked, setuptools should be installed on your system.
-If the command fails due to permission errors, you may need to be the
-administrative user on your system to successfully invoke the script. To
-remediate this, you may need to do:
+ After you install Python on Windows, you may need to add the ``C:\Python3x``
+ directory to your environment's ``Path``, where ``x`` is the minor version
+ of installed Python, in order to make it possible to invoke Python from a
+ command prompt by typing ``python``. To do so, right click ``My Computer``,
+ select ``Properties`` --> ``Advanced Tab`` --> ``Environment Variables`` and
+ add that directory to the end of the ``Path`` environment variable.
-.. code-block:: text
+ .. seealso:: See `Configuring Python (on Windows)
+ <https://docs.python.org/3/using/windows.html#configuring-python>`_ for
+ full details.
- $ sudo python ez_setup.py
.. index::
- pair: install; virtualenv
+ single: requirements for installing packages
-Installing the ``virtualenv`` Package
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. _requirements-for-installing-packages:
-Once you've got setuptools installed, you should install the :term:`virtualenv`
-package. To install the :term:`virtualenv` package into your
-setuptools-enabled Python interpreter, use the ``easy_install`` command.
-
-.. warning::
+Requirements for Installing Packages
+------------------------------------
- Python 3.3 includes ``pyvenv`` out of the box, which provides similar
- functionality to ``virtualenv``. We however suggest using ``virtualenv``
- instead, which works well with Python 3.3. This isn't a recommendation made
- for technical reasons; it's made because it's not feasible for the authors
- of this guide to explain setup using multiple virtual environment systems.
- We are aiming to not need to make the installation documentation
- Turing-complete.
-
- If you insist on using ``pyvenv``, you'll need to understand how to install
- software such as ``setuptools`` into the virtual environment manually, which
- this guide does not cover.
-
-.. code-block:: text
+Use :term:`pip` for installing packages and ``python3 -m venv env`` for
+creating a virtual environment. A virtual environment is a semi-isolated Python
+environment that allows packages to be installed for use by a particular
+application, rather than being installed system wide.
- $ easy_install virtualenv
+.. seealso:: See the Python Packaging Authority's (PyPA) documention
+ `Requirements for Installing Packages
+ <https://packaging.python.org/en/latest/installing/#requirements-for-installing-packages>`_
+ for full details.
-This command should succeed, and tell you that the virtualenv package is now
-installed. If it fails due to permission errors, you may need to install it as
-your system's administrative user. For example:
-
-.. code-block:: text
-
- $ sudo easy_install virtualenv
.. index::
- single: virtualenv
- pair: Python; virtual environment
-
-Creating the Virtual Python Environment
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Once the :term:`virtualenv` package is installed in your Python environment,
-you can then create a virtual environment. To do so, invoke the following:
-
-.. code-block:: text
-
- $ export VENV=~/env
- $ virtualenv $VENV
- New python executable in /home/foo/env/bin/python
- Installing setuptools.............done.
-
-You can either follow the use of the environment variable, ``$VENV``, or
-replace it with the root directory of the :term:`virtualenv`. In that case, the
-`export` command can be skipped. If you choose the former approach, ensure that
-it's an absolute path.
+ single: installing on UNIX
+ single: installing on Mac OS X
-.. warning::
+.. _installing_unix:
- Avoid using the ``--system-site-packages`` option when creating the
- virtualenv unless you know what you are doing. For versions of virtualenv
- prior to 1.7, make sure to use the ``--no-site-packages`` option, because
- this option was formerly not the default and may produce undesirable
- results.
+Installing :app:`Pyramid` on a UNIX System
+------------------------------------------
-.. warning::
+After installing Python as described previously in :ref:`for-mac-os-x-users` or
+:ref:`if-you-don-t-yet-have-a-python-interpreter-unix`, and satisfying the
+:ref:`requirements-for-installing-packages`, you can now install Pyramid.
- *do not* use ``sudo`` to run the ``virtualenv`` script. It's perfectly
- acceptable (and desirable) to create a virtualenv as a normal user.
+#. Make a :term:`virtual environment` workspace:
+ .. code-block:: bash
-Installing :app:`Pyramid` into the Virtual Python Environment
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ $ export VENV=~/env
+ $ python3 -m venv $VENV
-After you've got your virtualenv installed, you may install :app:`Pyramid`
-itself using the following commands:
+ You can either follow the use of the environment variable ``$VENV``, or
+ replace it with the root directory of the virtual environment. If you choose
+ the former approach, ensure that ``$VENV`` is an absolute path. In the
+ latter case, the ``export`` command can be skipped.
-.. parsed-literal::
+#. (Optional) Consider using ``$VENV/bin/activate`` to make your shell
+ environment wired to use the virtual environment.
- $ $VENV/bin/easy_install "pyramid==\ |release|\ "
+#. Use ``pip`` to get :app:`Pyramid` and its direct dependencies installed:
-The ``easy_install`` command will take longer than the previous ones to
-complete, as it downloads and installs a number of dependencies.
+ .. parsed-literal::
-.. note::
+ $ $VENV/bin/pip install "pyramid==\ |release|\ "
- If you see any warnings and/or errors related to failing to compile the C
- extensions, in most cases you may safely ignore those errors. If you wish to
- use the C extensions, please verify that you have a functioning compiler and
- the Python header files installed.
.. index::
single: installing on Windows
@@ -313,72 +180,38 @@ complete, as it downloads and installs a number of dependencies.
Installing :app:`Pyramid` on a Windows System
---------------------------------------------
-You can use Pyramid on Windows under Python 2 or 3.
+After installing Python as described previously in
+:ref:`if-you-don-t-yet-have-a-python-interpreter-windows`, and satisfying the
+:ref:`requirements-for-installing-packages`, you can now install Pyramid.
-#. Download and install the most recent `Python 2.7.x or 3.3.x version
- <http://www.python.org/download/>`_ for your system.
+#. Make a :term:`virtual environment` workspace:
-#. Download and install the `Python for Windows extensions
- <http://sourceforge.net/projects/pywin32/files/pywin32/>`_. Carefully read
- the README.txt file at the end of the list of builds, and follow its
- directions. Make sure you get the proper 32- or 64-bit build and Python
- version.
-
-#. Install latest :term:`setuptools` distribution into the Python from step 1
- above: download `ez_setup.py
- <https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py>`_ and run
- it using the ``python`` interpreter of your Python 2.7 or 3.3 installation
- using a command prompt:
-
- .. code-block:: text
-
- # modify the command according to the python version, e.g.:
- # for Python 2.7:
- c:\> c:\Python27\python ez_setup.py
- # for Python 3.3:
- c:\> c:\Python33\python ez_setup.py
-
-#. Install `virtualenv`:
-
- .. code-block:: text
-
- # modify the command according to the python version, e.g.:
- # for Python 2.7:
- c:\> c:\Python27\Scripts\easy_install virtualenv
- # for Python 3.3:
- c:\> c:\Python33\Scripts\easy_install virtualenv
-
-#. Make a :term:`virtualenv` workspace:
-
- .. code-block:: text
+ .. code-block:: doscon
c:\> set VENV=c:\env
- # modify the command according to the python version, e.g.:
- # for Python 2.7:
- c:\> c:\Python27\Scripts\virtualenv %VENV%
- # for Python 3.3:
- c:\> c:\Python33\Scripts\virtualenv %VENV%
+ # replace "x" with your minor version of Python 3
+ c:\> c:\Python3x\Scripts\python3 -m venv %VENV%
- You can either follow the use of the environment variable, ``%VENV%``, or
- replace it with the root directory of the :term:`virtualenv`. In that case,
- the `set` command can be skipped. If you choose the former approach, ensure
- that it's an absolute path.
+ You can either follow the use of the environment variable ``%VENV%``, or
+ replace it with the root directory of the virtual environment. If you choose
+ the former approach, ensure that ``%VENV%`` is an absolute path. In the
+ latter case, the ``set`` command can be skipped.
#. (Optional) Consider using ``%VENV%\Scripts\activate.bat`` to make your shell
- environment wired to use the virtualenv.
+ environment wired to use the virtual environment.
-#. Use ``easy_install`` to get :app:`Pyramid` and its direct dependencies
- installed:
+#. Use ``pip`` to get :app:`Pyramid` and its direct dependencies installed:
.. parsed-literal::
- c:\\env> %VENV%\\Scripts\\easy_install "pyramid==\ |release|\ "
+ c:\\env> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ "
+
What Gets Installed
-------------------
-When you ``easy_install`` :app:`Pyramid`, various other libraries such as
-WebOb, PasteDeploy, and others are installed.
+When you install :app:`Pyramid`, various libraries such as WebOb, PasteDeploy,
+and others are installed.
Additionally, as chronicled in :ref:`project_narr`, scaffolds will be
registered, which make it easy to start a new :app:`Pyramid` project.
diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst
index 8db52dc21..24c9f6b93 100644
--- a/docs/narr/introduction.rst
+++ b/docs/narr/introduction.rst
@@ -859,14 +859,15 @@ Testing
Every release of Pyramid has 100% statement coverage via unit and integration
tests, as measured by the ``coverage`` tool available on PyPI. It also has
greater than 95% decision/condition coverage as measured by the
-``instrumental`` tool available on PyPI. It is automatically tested by the
-Jenkins tool on Python 2.6, Python 2.7, Python 3.3, Python 3.4, Python 3.5,
-PyPy, and PyPy3 after each commit to its GitHub repository. Official Pyramid
-add-ons are held to a similar testing standard. We still find bugs in Pyramid
-and its official add-ons, but we've noticed we find a lot more of them while
-working on other projects that don't have a good testing regime.
-
-Example: http://jenkins.pylonsproject.org/
+``instrumental`` tool available on PyPI. It is automatically tested by Travis,
+and Jenkins on Python 2.7, Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3
+after each commit to its GitHub repository. Official Pyramid add-ons are held
+to a similar testing standard. We still find bugs in Pyramid and its official
+add-ons, but we've noticed we find a lot more of them while working on other
+projects that don't have a good testing regime.
+
+Travis: https://travis-ci.org/Pylons/pyramid
+Jenkins: http://jenkins.pylonsproject.org/job/pyramid/
Support
~~~~~~~
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index 923fde436..81fc9acf4 100644
--- a/docs/narr/project.rst
+++ b/docs/narr/project.rst
@@ -67,14 +67,14 @@ Creating the Project
.. seealso:: See also the output of :ref:`pcreate --help <pcreate_script>`.
In :ref:`installing_chapter`, you created a virtual Python environment via the
-``virtualenv`` command. To start a :app:`Pyramid` :term:`project`, use the
-``pcreate`` command installed within the virtualenv. We'll choose the
+``venv`` command. To start a :app:`Pyramid` :term:`project`, use the
+``pcreate`` command installed within the virtual environment. We'll choose the
``starter`` scaffold for this purpose. When we invoke ``pcreate``, it will
create a directory that represents our project.
-In :ref:`installing_chapter` we called the virtualenv directory ``env``. The
-following commands assume that our current working directory is the ``env``
-directory.
+In :ref:`installing_chapter` we called the virtual environment directory
+``env``. The following commands assume that our current working directory is
+the ``env`` directory.
The below example uses the ``pcreate`` command to create a project with the
``starter`` scaffold.
@@ -90,24 +90,13 @@ Or on Windows:
.. code-block:: text
> %VENV%\Scripts\pcreate -s starter MyProject
-
-Here's sample output from a run of ``pcreate`` on UNIX for a project we name
-``MyProject``:
-
-.. code-block:: bash
-
- $ $VENV/bin/pcreate -s starter MyProject
- Creating template pyramid
- Creating directory ./MyProject
- # ... more output ...
- Running /Users/chrism/projects/pyramid/bin/python setup.py egg_info
As a result of invoking the ``pcreate`` command, a directory named
``MyProject`` is created. That directory is a :term:`project` directory. The
``setup.py`` file in that directory can be used to distribute your application,
or install your application for deployment or development.
-A ``.ini`` file named ``development.ini`` will be created in the project
+An ``.ini`` file named ``development.ini`` will be created in the project
directory. You will use this ``.ini`` file to configure a server, to run your
application, and to debug your application. It contains configuration that
enables an interactive debugger and settings optimized for development.
@@ -123,16 +112,16 @@ The ``MyProject`` project directory contains an additional subdirectory named
which holds very simple :app:`Pyramid` sample code. This is where you'll edit
your application's Python code and templates.
-We created this project within an ``env`` virtualenv directory. However, note
-that this is not mandatory. The project directory can go more or less anywhere
-on your filesystem. You don't need to put it in a special "web server"
-directory, and you don't need to put it within a virtualenv directory. The
-author uses Linux mainly, and tends to put project directories which he creates
-within his ``~/projects`` directory. On Windows, it's a good idea to put
-project directories within a directory that contains no space characters, so
-it's wise to *avoid* a path that contains, i.e., ``My Documents``. As a
-result, the author, when he uses Windows, just puts his projects in
-``C:\projects``.
+We created this project within an ``env`` virtual environment directory.
+However, note that this is not mandatory. The project directory can go more or
+less anywhere on your filesystem. You don't need to put it in a special "web
+server" directory, and you don't need to put it within a virtual environment
+directory. The author uses Linux mainly, and tends to put project directories
+which he creates within his ``~/projects`` directory. On Windows, it's a good
+idea to put project directories within a directory that contains no space
+characters, so it's wise to *avoid* a path that contains, i.e., ``My
+Documents``. As a result, the author, when he uses Windows, just puts his
+projects in ``C:\projects``.
.. warning::
@@ -151,8 +140,9 @@ Installing your Newly Created Project for Development
To install a newly created project for development, you should ``cd`` to the
newly created project directory and use the Python interpreter from the
-:term:`virtualenv` you created during :ref:`installing_chapter` to invoke the
-command ``python setup.py develop``
+:term:`virtual environment` you created during :ref:`installing_chapter` to
+invoke the command ``pip install -e .``, which installs the project in
+development mode (``-e`` is for "editable") into the current directory (``.``).
The file named ``setup.py`` will be in the root of the pcreate-generated
project directory. The ``python`` you're invoking should be the one that lives
@@ -165,23 +155,24 @@ On UNIX:
.. code-block:: bash
$ cd MyProject
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
Or on Windows:
-.. code-block:: text
+.. code-block:: doscon
> cd MyProject
- > %VENV%\Scripts\python.exe setup.py develop
+ > %VENV%\Scripts\pip install -e .
Elided output from a run of this command on UNIX is shown below:
.. code-block:: bash
$ cd MyProject
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
...
- Finished processing dependencies for MyProject==0.0
+ Successfully installed Chameleon-2.24 Mako-1.0.4 MyProject \
+ pyramid-chameleon-0.3 pyramid-debugtoolbar-2.4.2 pyramid-mako-1.0.2
This will install a :term:`distribution` representing your project into the
virtual environment interpreter's library set so it can be found by ``import``
@@ -195,65 +186,54 @@ statements and by other console scripts such as ``pserve``, ``pshell``,
Running the Tests for Your Application
--------------------------------------
-To run unit tests for your application, you should invoke them using the Python
-interpreter from the :term:`virtualenv` you created during
-:ref:`installing_chapter` (the ``python`` command that lives in the ``bin``
-directory of your virtualenv).
+To run unit tests for your application, you must first install the testing
+dependencies.
On UNIX:
.. code-block:: bash
- $ $VENV/bin/python setup.py test -q
+ $ $VENV/bin/pip install -e ".[testing]"
-Or on Windows:
+On Windows:
-.. code-block:: text
+.. code-block:: doscon
+
+ > %VENV%\Scripts\pip install -e ".[testing]"
+
+Once the testing requirements are installed, then you can run the tests using
+the ``py.test`` command that was just installed in the ``bin`` directory of
+your virtual environment.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ $VENV/bin/py.test myproject/tests.py -q
+
+On Windows:
+
+.. code-block:: doscon
- > %VENV%\Scripts\python.exe setup.py test -q
+ > %VENV%\Scripts\py.test myproject\tests.py -q
Here's sample output from a test run on UNIX:
.. code-block:: bash
- $ $VENV/bin/python setup.py test -q
- running test
- running egg_info
- writing requirements to MyProject.egg-info/requires.txt
- writing MyProject.egg-info/PKG-INFO
- writing top-level names to MyProject.egg-info/top_level.txt
- writing dependency_links to MyProject.egg-info/dependency_links.txt
- writing entry points to MyProject.egg-info/entry_points.txt
- reading manifest file 'MyProject.egg-info/SOURCES.txt'
- reading manifest template 'MANIFEST.in'
- warning: no files found matching '*.cfg'
- warning: no files found matching '*.rst'
- warning: no files found matching '*.ico' under directory 'myproject'
- warning: no files found matching '*.gif' under directory 'myproject'
- warning: no files found matching '*.jpg' under directory 'myproject'
- warning: no files found matching '*.txt' under directory 'myproject'
- warning: no files found matching '*.mak' under directory 'myproject'
- warning: no files found matching '*.mako' under directory 'myproject'
- warning: no files found matching '*.js' under directory 'myproject'
- warning: no files found matching '*.html' under directory 'myproject'
- warning: no files found matching '*.xml' under directory 'myproject'
- writing manifest file 'MyProject.egg-info/SOURCES.txt'
- running build_ext
- .
- ----------------------------------------------------------------------
- Ran 1 test in 0.008s
-
- OK
+ $ $VENV/bin/py.test myproject/tests.py -q
+ ..
+ 2 passed in 0.47 seconds
The tests themselves are found in the ``tests.py`` module in your ``pcreate``
-generated project. Within a project generated by the ``starter`` scaffold, a
-single sample test exists.
+generated project. Within a project generated by the ``starter`` scaffold,
+only two sample tests exist.
.. note::
- The ``-q`` option is passed to the ``setup.py test`` command to limit the
- output to a stream of dots. If you don't pass ``-q``, you'll see more
- verbose test result output (which normally isn't very useful).
+ The ``-q`` option is passed to the ``py.test`` command to limit the output
+ to a stream of dots. If you don't pass ``-q``, you'll see verbose test
+ result output (which normally isn't very useful).
.. index::
single: running an application
@@ -432,9 +412,8 @@ like this to enable the toolbar when your system contacts Pyramid:
# .. other settings ...
debugtoolbar.hosts = 192.168.1.1
-For more information about what the debug toolbar allows you to do, see `the
-documentation for pyramid_debugtoolbar
-<http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest/>`_.
+For more information about what the debug toolbar allows you to do, see the
+:ref:`documentation for pyramid_debugtoolbar <toolbar:overview>`.
The debug toolbar will not be shown (and all debugging will be turned off) when
you use the ``production.ini`` file instead of the ``development.ini`` ini file
@@ -688,16 +667,16 @@ control system, you may need to install a setuptools add-on such as
~~~~~~~~~~~~
The ``setup.py`` file is a :term:`setuptools` setup file. It is meant to be
-run directly from the command line to perform a variety of functions, such as
-testing, packaging, and distributing your application.
+used to define requirements for installing dependencies for your package and
+testing, as well as distributing your application.
.. note::
``setup.py`` is the de facto standard which Python developers use to
distribute their reusable code. You can read more about ``setup.py`` files
- and their usage in the `Setuptools documentation
- <http://peak.telecommunity.com/DevCenter/setuptools>`_ and `Python Packaging
- User Guide <https://packaging.python.org/en/latest/>`_.
+ and their usage in the `Python Packaging User Guide
+ <https://packaging.python.org/en/latest/>`_ and `Setuptools documentation
+ <http://pythonhosted.org/setuptools/>`_.
Our generated ``setup.py`` looks like this:
@@ -706,7 +685,7 @@ Our generated ``setup.py`` looks like this:
:linenos:
The ``setup.py`` file calls the setuptools ``setup`` function, which does
-various things depending on the arguments passed to ``setup.py`` on the command
+various things depending on the arguments passed to ``pip`` on the command
line.
Within the arguments to this function call, information about your application
@@ -717,8 +696,8 @@ exists in this file in this section.
Your application's name can be any string; it is specified in the ``name``
field. The version number is specified in the ``version`` value. A short
description is provided in the ``description`` field. The ``long_description``
-is conventionally the content of the README and CHANGES file appended together.
-The ``classifiers`` field is a list of `Trove
+is conventionally the content of the ``README`` and ``CHANGES`` files appended
+together. The ``classifiers`` field is a list of `Trove
<http://pypi.python.org/pypi?%3Aaction=list_classifiers>`_ classifiers
describing your application. ``author`` and ``author_email`` are text fields
which probably don't need any description. ``url`` is a field that should
@@ -726,14 +705,13 @@ point at your application project's URL (if any). ``packages=find_packages()``
causes all packages within the project to be found when packaging the
application. ``include_package_data`` will include non-Python files when the
application is packaged if those files are checked into version control.
-``zip_safe`` indicates that this package is not safe to use as a zipped egg;
-instead it will always unpack as a directory, which is more convenient.
-``install_requires`` and ``tests_require`` indicate that this package depends
-on the ``pyramid`` package. ``test_suite`` points at the package for our
-application, which means all tests found in the package will be run when
-``setup.py test`` is invoked. We examined ``entry_points`` in our discussion
-of the ``development.ini`` file; this file defines the ``main`` entry point
-that represents our project's application.
+``zip_safe=False`` indicates that this package is not safe to use as a zipped
+egg; instead it will always unpack as a directory, which is more convenient.
+``install_requires`` indicate that this package depends on the ``pyramid``
+package. ``extras_require`` is a Python dictionary that defines what is
+required to be installed for running tests. We examined ``entry_points`` in our
+discussion of the ``development.ini`` file; this file defines the ``main``
+entry point that represents our project's application.
Usually you only need to think about the contents of the ``setup.py`` file when
distributing your application to other people, when adding Python package
@@ -745,7 +723,7 @@ you can try this command now:
$ $VENV/bin/python setup.py sdist
This will create a tarball of your application in a ``dist`` subdirectory named
-``MyProject-0.1.tar.gz``. You can send this tarball to other people who want
+``MyProject-0.0.tar.gz``. You can send this tarball to other people who want
to install and use your application.
.. index::
@@ -928,13 +906,13 @@ The ``tests.py`` module includes unit tests for your application.
.. literalinclude:: MyProject/myproject/tests.py
:language: python
- :lines: 1-17
:linenos:
-This sample ``tests.py`` file has a single unit test defined within it. This
-test is executed when you run ``python setup.py test``. You may add more tests
-here as you build your application. You are not required to write tests to use
-:app:`Pyramid`. This file is simply provided for convenience and example.
+This sample ``tests.py`` file has one unit test and one functional test defined
+within it. These tests are executed when you run ``py.test myproject/tests.py
+-q``. You may add more tests here as you build your application. You are not
+required to write tests to use :app:`Pyramid`. This file is simply provided for
+convenience and example.
See :ref:`testing_chapter` for more information about writing :app:`Pyramid`
unit tests.
diff --git a/docs/narr/router.rst b/docs/narr/router.rst
index e02142e6e..e45e6f4a8 100644
--- a/docs/narr/router.rst
+++ b/docs/narr/router.rst
@@ -41,19 +41,24 @@ request enters a :app:`Pyramid` application through to the point that
user-defined :term:`route` matches the current WSGI environment. The
:term:`router` passes the request as an argument to the mapper.
-#. If any route matches, the route mapper adds attributes to the request:
- ``matchdict`` and ``matched_route`` attributes are added to the request
- object. The former contains a dictionary representing the matched dynamic
- elements of the request's ``PATH_INFO`` value, and the latter contains the
+#. If any route matches, the route mapper adds the attributes ``matchdict``
+ and ``matched_route`` to the request object. The former contains a
+ dictionary representing the matched dynamic elements of the request's
+ ``PATH_INFO`` value, and the latter contains the
:class:`~pyramid.interfaces.IRoute` object representing the route which
- matched. The root object associated with the route found is also generated:
- if the :term:`route configuration` which matched has an associated
- ``factory`` argument, this factory is used to generate the root object,
- otherwise a default :term:`root factory` is used.
+ matched.
-#. If a route match was *not* found, and a ``root_factory`` argument was passed
+#. A :class:`~pyramid.events.BeforeTraversal` :term:`event` is sent to any
+ subscribers.
+
+#. Continuing, if any route matches, the root object associated with the found
+ route is generated. If the :term:`route configuration` which matched has an
+ associated ``factory`` argument, then this factory is used to generate the
+ root object; otherwise a default :term:`root factory` is used.
+
+ However, if no route matches, and if a ``root_factory`` argument was passed
to the :term:`Configurator` constructor, that callable is used to generate
- the root object. If the ``root_factory`` argument passed to the
+ the root object. If the ``root_factory`` argument passed to the
Configurator constructor was ``None``, a default root factory is used to
generate a root object.
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index db554a93b..7cf96ac7d 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -367,6 +367,21 @@ Or include it as a header in a jQuery AJAX request:
The handler for the URL that receives the request should then require that the
correct CSRF token is supplied.
+.. index::
+ single: session.new_csrf_token
+
+Using the ``session.new_csrf_token`` Method
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To explicitly create a new CSRF token, use the ``session.new_csrf_token()``
+method. This differs only from ``session.get_csrf_token()`` inasmuch as it
+clears any existing CSRF token, creates a new CSRF token, sets the token into
+the session, and returns the token.
+
+.. code-block:: python
+
+ token = request.session.new_csrf_token()
+
Checking CSRF Tokens Manually
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -376,8 +391,8 @@ will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally,
you can specify ``raises=False`` to have the check return ``False`` instead of
raising an exception.
-By default, it checks for a GET or POST parameter named ``csrf_token`` or a
-header named ``X-CSRF-Token``.
+By default, it checks for a POST parameter named ``csrf_token`` or a header
+named ``X-CSRF-Token``.
.. code-block:: python
@@ -389,12 +404,59 @@ header named ``X-CSRF-Token``.
# ...
-.. index::
- single: session.new_csrf_token
+.. _auto_csrf_checking:
+
+Checking CSRF Tokens Automatically
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.7
+
+:app:`Pyramid` supports automatically checking CSRF tokens on requests with an
+unsafe method as defined by RFC2616. Any other request may be checked manually.
+This feature can be turned on globally for an application using the
+``pyramid.require_default_csrf`` setting.
+
+If the ``pyramid.required_default_csrf`` setting is a :term:`truthy string` or
+``True`` then the default CSRF token parameter will be ``csrf_token``. If a
+different token is desired, it may be passed as the value. Finally, a
+:term:`falsey string` or ``False`` will turn off automatic CSRF checking
+globally on every request.
+
+No matter what, CSRF checking may be explicitly enabled or disabled on a
+per-view basis using the ``require_csrf`` view option. This option is of the
+same format as the ``pyramid.require_default_csrf`` setting, accepting strings
+or boolean values.
+
+If ``require_csrf`` is ``True`` but does not explicitly define a token to
+check, then the token name is pulled from whatever was set in the
+``pyramid.require_default_csrf`` setting. Finally, if that setting does not
+explicitly define a token, then ``csrf_token`` is the token required. This token
+name will be required in ``request.POST`` which is the submitted form body.
+
+It is always possible to pass the token in the ``X-CSRF-Token`` header as well.
+There is currently no way to define an alternate name for this header without
+performing CSRF checking manually.
+
+In addition to token based CSRF checks, the automatic CSRF checking will also
+check the referrer of the request to ensure that it matches one of the trusted
+origins. By default the only trusted origin is the current host, however
+additional origins may be configured by setting
+``pyramid.csrf_trusted_origins`` to a list of domain names (and ports if they
+are non standard). If a host in the list of domains starts with a ``.`` then
+that will allow all subdomains as well as the domain without the ``.``.
+
+If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` exception
+will be raised. This exception may be caught and handled by an
+:term:`exception view` but, by default, will result in a ``400 Bad Request``
+response being sent to the client.
Checking CSRF Tokens with a View Predicate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. deprecated:: 1.7
+ Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead
+ to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised.
+
A convenient way to require a valid CSRF token for a particular view is to
include ``check_csrf=True`` as a view predicate. See
:meth:`pyramid.config.Configurator.add_view`.
@@ -410,15 +472,3 @@ include ``check_csrf=True`` as a view predicate. See
predicate system, when it doesn't find a view, raises ``HTTPNotFound``
instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different
from calling :func:`pyramid.session.check_csrf_token`.
-
-Using the ``session.new_csrf_token`` Method
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To explicitly create a new CSRF token, use the ``session.new_csrf_token()``
-method. This differs only from ``session.get_csrf_token()`` inasmuch as it
-clears any existing CSRF token, creates a new CSRF token, sets the token into
-the session, and returns the token.
-
-.. code-block:: python
-
- token = request.session.new_csrf_token()
diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst
index daa3cc43f..7c847de50 100644
--- a/docs/narr/subrequest.rst
+++ b/docs/narr/subrequest.rst
@@ -279,3 +279,53 @@ within a tween, because tweens already, by definition, have access to a
function that will cause a subrequest (they are passed a ``handle`` function).
It's fine to invoke :meth:`~pyramid.request.Request.invoke_subrequest` from
within an event handler, however.
+
+
+.. index::
+ pair: subrequest; exception view
+
+Invoking an Exception View
+--------------------------
+
+.. versionadded:: 1.7
+
+:app:`Pyramid` apps may define :term:`exception views <exception view>` which
+can handle any raised exceptions that escape from your code while processing
+a request. By default an unhandled exception will be caught by the ``EXCVIEW``
+:term:`tween`, which will then lookup an exception view that can handle the
+exception type, generating an appropriate error response.
+
+In :app:`Pyramid` 1.7 the :meth:`pyramid.request.Request.invoke_exception_view`
+was introduced, allowing a user to invoke an exception view while manually
+handling an exception. This can be useful in a few different circumstances:
+
+- Manually handling an exception losing the current call stack or flow.
+
+- Handling exceptions outside of the context of the ``EXCVIEW`` tween. The
+ tween only covers certain parts of the request processing pipeline (See
+ :ref:`router_chapter`). There are also some corner cases where an exception
+ can be raised that will still bubble up to middleware, and possibly to the
+ web server in which case a generic ``500 Internal Server Error`` will be
+ returned to the client.
+
+Below is an example usage of
+:meth:`pyramid.request.Request.invoke_exception_view`:
+
+.. code-block:: python
+ :linenos:
+
+ def foo(request):
+ try:
+ some_func_that_errors()
+ return response
+ except Exception:
+ response = request.invoke_exception_view()
+ if response is not None:
+ return response
+ else:
+ # there is no exception view for this exception, simply
+ # re-raise and let someone else handle it
+ raise
+
+Please note that in most cases you do not need to write code like this, and you
+may rely on the ``EXCVIEW`` tween to handle this for you.
diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst
index a3f62058b..354a462d4 100644
--- a/docs/narr/testing.rst
+++ b/docs/narr/testing.rst
@@ -275,7 +275,7 @@ without needing to invoke the actual application configuration implied by its
In the above example, we create a ``MyTest`` test case that inherits from
:class:`unittest.TestCase`. If it's in our :app:`Pyramid` application, it will
-be found when ``setup.py test`` is run. It has two test methods.
+be found when ``py.test`` is run. It has two test methods.
The first test method, ``test_view_fn_forbidden`` tests the ``view_fn`` when
the authentication policy forbids the current user the ``edit`` permission. Its
@@ -365,46 +365,37 @@ Functional tests test your literal application.
In Pyramid, functional tests are typically written using the :term:`WebTest`
package, which provides APIs for invoking HTTP(S) requests to your application.
+We also like ``py.test`` and ``pytest-cov`` to provide simple testing and
+coverage reports.
-Regardless of which testing :term:`package` you use, ensure to add a
-``tests_require`` dependency on that package to your application's
-``setup.py`` file. Using the project ``MyProject`` generated by the starter
-scaffold as described in :doc:`project`, we would insert the following code immediately following the
-``requires`` block in the file ``MyProject/setup.py``.
+Regardless of which testing :term:`package` you use, be sure to add a
+``tests_require`` dependency on that package to your application's ``setup.py``
+file. Using the project ``MyProject`` generated by the starter scaffold as
+described in :doc:`project`, we would insert the following code immediately
+following the ``requires`` block in the file ``MyProject/setup.py``.
-.. code-block:: ini
+.. literalinclude:: MyProject/setup.py
+ :language: python
:linenos:
+ :lines: 11-22
:lineno-start: 11
:emphasize-lines: 8-
- requires = [
- 'pyramid',
- 'pyramid_chameleon',
- 'pyramid_debugtoolbar',
- 'waitress',
- ]
-
- test_requires = [
- 'webtest',
- ]
-
Remember to change the dependency.
-.. code-block:: ini
+.. literalinclude:: MyProject/setup.py
+ :language: python
:linenos:
- :lineno-start: 39
- :emphasize-lines: 2
-
- install_requires=requires,
- tests_require=test_requires,
- test_suite="myproject",
+ :lines: 40-44
+ :lineno-start: 40
+ :emphasize-lines: 2-4
-As always, whenever you change your dependencies, make sure to run the
-following command.
+As always, whenever you change your dependencies, make sure to run the correct
+``pip install -e`` command.
.. code-block:: bash
- $VENV/bin/python setup.py develop
+ $VENV/bin/pip install -e ".[testing]"
In your ``MyPackage`` project, your :term:`package` is named ``myproject``
which contains a ``views`` module, which in turn contains a :term:`view`
diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst
index cacfba92a..fcdce4f8d 100644
--- a/docs/narr/upgrading.rst
+++ b/docs/narr/upgrading.rst
@@ -127,8 +127,6 @@ you can see DeprecationWarnings printed to the console when the tests run.
$ python -Wd setup.py test -q
The ``-Wd`` argument tells Python to print deprecation warnings to the console.
-Note that the ``-Wd`` flag is only required for Python 2.7 and better: Python
-versions 2.6 and older print deprecation warnings to the console by default.
See `the Python -W flag documentation
<http://docs.python.org/using/cmdline.html#cmdoption-W>`_ for more information.
diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index 0bd52b6e2..cd5b8feb0 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -192,6 +192,36 @@ Non-Predicate Arguments
only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` with
the first element of ``None``, i.e., ``(None, {'public':True})``.
+
+``require_csrf``
+
+ CSRF checks will affect any request method that is not defined as a "safe"
+ method by RFC2616. In pratice this means that GET, HEAD, OPTIONS, and TRACE
+ methods will pass untouched and all others methods will require CSRF. This
+ option is used in combination with the ``pyramid.require_default_csrf``
+ setting to control which request parameters are checked for CSRF tokens.
+
+ This feature requires a configured :term:`session factory`.
+
+ If this option is set to ``True`` then CSRF checks will be enabled for POST
+ requests to this view. The required token will be whatever was specified by
+ the ``pyramid.require_default_csrf`` setting, or will fallback to
+ ``csrf_token``.
+
+ If this option is set to a string then CSRF checks will be enabled and it
+ will be used as the required token regardless of the
+ ``pyramid.require_default_csrf`` setting.
+
+ If this option is set to ``False`` then CSRF checks will be disabled
+ regardless of the ``pyramid.require_default_csrf`` setting.
+
+ In addition, if this option is set to ``True`` or a string then CSRF origin
+ checking will be enabled.
+
+ See :ref:`auto_csrf_checking` for more information.
+
+ .. versionadded:: 1.7
+
``wrapper``
The :term:`view name` of a different :term:`view configuration` which will
receive the response body of this view as the ``request.wrapped_body``
@@ -433,7 +463,7 @@ configured view.
check name.
If CSRF checking is performed, the checked value will be the value of
- ``request.params[check_name]``. This value will be compared against the
+ ``request.POST[check_name]``. This value will be compared against the
value of ``request.session.get_csrf_token()``, and the check will pass if
these two values are the same. If the check passes, the associated view will
be permitted to execute. If the check fails, the associated view will not be
diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst
index a7c184776..78af6fd40 100644
--- a/docs/quick_tour.rst
+++ b/docs/quick_tour.rst
@@ -15,43 +15,46 @@ Installation
Once you have a standard Python environment setup, getting started with Pyramid
is a breeze. Unfortunately "standard" is not so simple in Python. For this
-Quick Tour, it means `Python <https://www.python.org/downloads/>`_, a `virtual
-environment <http://docs.python.org/dev/library/venv.html>`_ (or `virtualenv
-for Python 2.7 <https://pypi.python.org/pypi/virtualenv>`_), and `setuptools
-<https://pypi.python.org/pypi/setuptools/>`_.
+Quick Tour, it means `Python <https://www.python.org/downloads/>`_, `venv
+<https://packaging.python.org/en/latest/projects/#venv>`_ (or `virtualenv for
+Python 2.7 <https://packaging.python.org/en/latest/projects/#virtualenv>`_),
+`pip <https://packaging.python.org/en/latest/projects/#pip>`_, and `setuptools
+<https://packaging.python.org/en/latest/projects/#easy-install>`_.
-As an example, for Python 3.3+ on Linux:
+To save a little bit of typing and to be certain that we use the modules,
+scripts, and packages installed in our virtual environment, we'll set an
+environment variable, too.
+
+As an example, for Python 3.5+ on Linux:
.. parsed-literal::
- $ pyvenv env33
- $ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | env33/bin/python
- $ env33/bin/easy_install "pyramid==\ |release|\ "
+ # set an environment variable to where you want your virtual environment
+ $ export VENV=~/env
+ # create the virtual environment
+ $ python3 -m venv $VENV
+ # install pyramid
+ $ $VENV/bin/pip install pyramid
+ # or for a specific released version
+ $ $VENV/bin/pip install "pyramid==\ |release|\ "
For Windows:
.. parsed-literal::
- # Use your browser to download:
- # https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py
- c:\\> c:\\Python33\\python -m venv env33
- c:\\> env33\\Scripts\\python ez_setup.py
- c:\\> env33\\Scripts\\easy_install "pyramid==\ |release|\ "
+ # set an environment variable to where you want your virtual environment
+ c:\> set VENV=c:\env
+ # create the virtual environment
+ c:\\> c:\\Python35\\python3 -m venv %VENV%
+ # install pyramid
+ c:\\> %VENV%\\Scripts\\pip install pyramid
+ # or for a specific released version
+ c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ "
Of course Pyramid runs fine on Python 2.6+, as do the examples in this *Quick
-Tour*. We're just showing Python 3 a little love (Pyramid had production
-support for Python 3 in October 2011).
-
-.. note::
-
- Why ``easy_install`` and not ``pip``? Some distributions upon which
- Pyramid depends have optional C extensions for performance. ``pip`` cannot
- install some binary Python distributions. With ``easy_install``, Windows
- users are able to obtain binary Python distributions, so they get the
- benefit of the C extensions without needing a C compiler. Also there can
- be issues when ``pip`` and ``easy_install`` are used side-by-side in the
- same environment, so we chose to recommend ``easy_install`` for the sake of
- reducing the complexity of these instructions.
+Tour*. We're showing Python 3 for simplicity. (Pyramid had production support
+for Python 3 in October 2011.) Also for simplicity, the remaining examples will
+show only UNIX commands.
.. seealso:: See also:
:ref:`Quick Tutorial section on Requirements <qtut_requirements>`,
@@ -72,7 +75,7 @@ This simple example is easy to run. Save this as ``app.py`` and run it:
.. code-block:: bash
- $ python ./app.py
+ $ $VENV/bin/python ./app.py
Next open http://localhost:6543/ in a browser, and you will see the ``Hello
World!`` message.
@@ -121,7 +124,9 @@ Let's see some features of requests and responses in action:
In this Pyramid view, we get the URL being visited from ``request.url``. Also
if you visited http://localhost:6543/?name=alice in a browser, the name is
-included in the body of the response::
+included in the body of the response:
+
+.. code-block:: text
URL http://localhost:6543/?name=alice with name: alice
@@ -249,7 +254,7 @@ Chameleon as a :term:`renderer` in our Pyramid application:
.. code-block:: bash
- $ easy_install pyramid_chameleon
+ $ $VENV/bin/pip install pyramid_chameleon
With the package installed, we can include the template bindings into our
configuration in ``app.py``:
@@ -293,7 +298,7 @@ Jinja2 as a :term:`renderer` in our Pyramid applications:
.. code-block:: bash
- $ easy_install pyramid_jinja2
+ $ $VENV/bin/pip install pyramid_jinja2
With the package installed, we can include the template bindings into our
configuration:
@@ -502,7 +507,7 @@ We next use the normal Python command to set up our package for development:
.. code-block:: bash
$ cd hello_world
- $ python ./setup.py develop
+ $ $VENV/bin/pip install -e .
We are moving in the direction of a full-featured Pyramid project, with a
proper setup for Python standards (packaging) and Pyramid configuration. This
@@ -510,7 +515,7 @@ includes a new way of running your application:
.. code-block:: bash
- $ pserve development.ini
+ $ $VENV/bin/pserve development.ini
Let's look at ``pserve`` and configuration in more depth.
@@ -537,7 +542,7 @@ the server when they change:
.. code-block:: bash
- $ pserve development.ini --reload
+ $ $VENV/bin/pserve development.ini --reload
The ``pserve`` command has a number of other options and operations. Most of
the work, though, comes from your project's wiring, as expressed in the
@@ -617,7 +622,7 @@ It was installed when you previously ran:
.. code-block:: bash
- $ python ./setup.py develop
+ $ $VENV/bin/pip install -e .
The ``pyramid_debugtoolbar`` package is a Pyramid add-on, which means we need
to include its configuration into our web application. The ``pyramid_jinja2``
@@ -648,48 +653,79 @@ the relevant ``.ini`` configuration file.
:ref:`Quick Tutorial pyramid_debugtoolbar <qtut_debugtoolbar>` and
:ref:`pyramid_debugtoolbar <toolbar:overview>`
-Unit tests and ``nose``
-=======================
+Unit tests and ``py.test``
+==========================
Yikes! We got this far and we haven't yet discussed tests. This is particularly
egregious, as Pyramid has had a deep commitment to full test coverage since
before its release.
Our ``pyramid_jinja2_starter`` scaffold generated a ``tests.py`` module with
-one unit test in it. To run it, let's install the handy ``nose`` test runner by
-editing ``setup.py``. While we're at it, we'll throw in the ``coverage`` tool
-which yells at us for code that isn't tested. Edit line 36 so it becomes the
-following:
+one unit test in it. To run it, let's install the handy ``pytest`` test runner
+by editing ``setup.py``. While we're at it, we'll throw in the ``pytest-cov``
+tool which yells at us for code that isn't tested. Insert and edit the
+following lines as shown:
.. code-block:: python
:linenos:
- :lineno-start: 36
+ :lineno-start: 11
+ :emphasize-lines: 8-12
+
+ requires = [
+ 'pyramid',
+ 'pyramid_jinja2',
+ 'pyramid_debugtoolbar',
+ 'waitress',
+ ]
+
+ tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
- tests_require={
- 'testing': ['nose', 'coverage'],
- },
+.. code-block:: python
+ :linenos:
+ :lineno-start: 34
+ :emphasize-lines: 2-4
-We changed ``setup.py`` which means we need to rerun
-``python ./setup.py develop``. We can now run all our tests:
+ zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
+
+We changed ``setup.py`` which means we need to rerun ``$VENV/bin/pip install -e
+".[testing]"``. We can now run all our tests:
.. code-block:: bash
- $ nosetests hello_world/tests.py
- .
- Name Stmts Miss Cover Missing
- ---------------------------------------------------
- hello_world 11 8 27% 11-23
- hello_world.models 5 1 80% 8
- hello_world.tests 14 0 100%
- hello_world.views 4 0 100%
- ---------------------------------------------------
- TOTAL 34 9 74%
- ----------------------------------------------------------------------
- Ran 1 test in 0.009s
+ $ $VENV/bin/py.test --cov=hello_world --cov-report=term-missing hello_world/tests.py
+
+This yields the following output.
+
+.. code-block:: text
+
+ =========================== test session starts ===========================
+ platform darwin -- Python 3.5.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: /Users/stevepiercy/projects/hack-on-pyramid/hello_world, inifile:
+ plugins: cov-2.2.1
+ collected 1 items
+
+ hello_world/tests.py .
+ ------------- coverage: platform darwin, python 3.5.0-final-0 -------------
+ Name Stmts Miss Cover Missing
+ --------------------------------------------------------
+ hello_world/__init__.py 11 8 27% 11-23
+ hello_world/resources.py 5 1 80% 8
+ hello_world/tests.py 14 0 100%
+ hello_world/views.py 4 0 100%
+ --------------------------------------------------------
+ TOTAL 34 9 74%
- OK
+ ========================= 1 passed in 0.22 seconds =========================
-Our unit test passed. What did our test look like?
+Our unit test passed, although its coverage is incomplete. What did our test
+look like?
.. literalinclude:: quick_tour/package/hello_world/tests.py
:linenos:
@@ -746,7 +782,9 @@ These emphasized sections in the configuration file:
Our application, a package named ``hello_world``, is set up 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::
+http://localhost:6543, your console will now show:
+
+.. code-block:: text
2016-01-18 13:55:55,040 DEBUG [hello_world.views:10][waitress] Some Message
@@ -825,9 +863,9 @@ Pyramid and SQLAlchemy are great friends. That friendship includes a scaffold!
.. code-block:: bash
- $ pcreate --scaffold alchemy sqla_demo
+ $ $VENV/bin/pcreate --scaffold alchemy sqla_demo
$ cd sqla_demo
- $ python setup.py develop
+ $ $VENV/bin/pip install -e .
We now have a working sample SQLAlchemy application with all dependencies
installed. The sample project provides a console script to initialize a SQLite
@@ -835,28 +873,23 @@ database with tables. Let's run it, then start the application:
.. code-block:: bash
- $ initialize_sqla_demo_db development.ini
- $ pserve development.ini
+ $ $VENV/bin/initialize_sqla_demo_db development.ini
+ $ $VENV/bin/pserve development.ini
The ORM eases the mapping of database structures into a programming language.
SQLAlchemy uses "models" for this mapping. The scaffold generated a sample
model:
-.. literalinclude:: quick_tour/sqla_demo/sqla_demo/models.py
- :language: python
- :linenos:
- :lineno-start: 21
- :lines: 21-
+.. literalinclude:: quick_tour/sqla_demo/sqla_demo/models/mymodel.py
+ :start-after: Start Sphinx Include
+ :end-before: End Sphinx Include
View code, which mediates the logic between web requests and the rest of the
system, can then easily get at the data thanks to SQLAlchemy:
-.. literalinclude:: quick_tour/sqla_demo/sqla_demo/views.py
- :language: python
- :linenos:
- :lineno-start: 12
- :lines: 12-18
- :emphasize-lines: 4
+.. literalinclude:: quick_tour/sqla_demo/sqla_demo/views/default.py
+ :start-after: Start Sphinx Include
+ :end-before: End Sphinx Include
.. seealso:: See also:
:ref:`Quick Tutorial Databases <qtut_databases>`, `SQLAlchemy
diff --git a/docs/quick_tour/sqla_demo/README.txt b/docs/quick_tour/sqla_demo/README.txt
index c7f9d6474..b6d4c7798 100644
--- a/docs/quick_tour/sqla_demo/README.txt
+++ b/docs/quick_tour/sqla_demo/README.txt
@@ -6,7 +6,7 @@ Getting Started
- cd <directory containing this file>
-- $VENV/bin/python setup.py develop
+- $VENV/bin/pip install -e .
- $VENV/bin/initialize_sqla_demo_db development.ini
diff --git a/docs/quick_tour/sqla_demo/development.ini b/docs/quick_tour/sqla_demo/development.ini
index cdf20638e..0db0950a0 100644
--- a/docs/quick_tour/sqla_demo/development.ini
+++ b/docs/quick_tour/sqla_demo/development.ini
@@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/sqla_demo.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
diff --git a/docs/quick_tour/sqla_demo/setup.py b/docs/quick_tour/sqla_demo/setup.py
index a9a8842e2..312a97c06 100644
--- a/docs/quick_tour/sqla_demo/setup.py
+++ b/docs/quick_tour/sqla_demo/setup.py
@@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
requires = [
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/__init__.py
index 867049e4f..7994bbfa8 100644
--- a/docs/quick_tour/sqla_demo/sqla_demo/__init__.py
+++ b/docs/quick_tour/sqla_demo/sqla_demo/__init__.py
@@ -1,20 +1,12 @@
from pyramid.config import Configurator
-from sqlalchemy import engine_from_config
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
config = Configurator(settings=settings)
- config.include('pyramid_chameleon')
+ config.include('pyramid_jinja2')
+ config.include('.models.meta')
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('home', '/')
config.scan()
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models.py b/docs/quick_tour/sqla_demo/sqla_demo/models.py
deleted file mode 100644
index a0d3e7b71..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/models.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from sqlalchemy import (
- Column,
- Index,
- 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 MyModel(Base):
- __tablename__ = 'models'
- id = Column(Integer, primary_key=True)
- name = Column(Text)
- value = Column(Integer)
-
-Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py
new file mode 100644
index 000000000..6ffc10a78
--- /dev/null
+++ b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py
@@ -0,0 +1,7 @@
+from sqlalchemy.orm import configure_mappers
+# import all models classes here for sqlalchemy mappers
+# to pick up
+from .mymodel import MyModel # flake8: noqa
+
+# run configure mappers to ensure we avoid any race conditions
+configure_mappers()
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py
new file mode 100644
index 000000000..80ececd8c
--- /dev/null
+++ b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py
@@ -0,0 +1,49 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.schema import MetaData
+import zope.sqlalchemy
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
+
+
+def includeme(config):
+ settings = config.get_settings()
+ dbmaker = get_dbmaker(get_engine(settings))
+
+ config.add_request_method(
+ lambda r: get_session(r.tm, dbmaker),
+ 'dbsession',
+ reify=True
+ )
+
+ config.include('pyramid_tm')
+
+
+def get_session(transaction_manager, dbmaker):
+ dbsession = dbmaker()
+ zope.sqlalchemy.register(dbsession,
+ transaction_manager=transaction_manager)
+ return dbsession
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_dbmaker(engine):
+ dbmaker = sessionmaker()
+ dbmaker.configure(bind=engine)
+ return dbmaker
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py b/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py
new file mode 100644
index 000000000..eb645bfe6
--- /dev/null
+++ b/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py
@@ -0,0 +1,19 @@
+from .meta import Base
+from sqlalchemy import (
+ Column,
+ Index,
+ Integer,
+ Text,
+)
+
+
+# Start Sphinx Include
+class MyModel(Base):
+ __tablename__ = 'models'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text)
+ value = Column(Integer)
+ # End Sphinx Include
+
+
+Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py b/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py
index 7dfdece15..f0d09729e 100644
--- a/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py
+++ b/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py
@@ -2,8 +2,6 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
@@ -11,11 +9,13 @@ from pyramid.paster import (
from pyramid.scripts.common import parse_vars
-from ..models import (
- DBSession,
- MyModel,
+from ..models.meta import (
Base,
+ get_session,
+ get_engine,
+ get_dbmaker,
)
+from ..models.mymodel import MyModel
def usage(argv):
@@ -32,9 +32,14 @@ def main(argv=sys.argv):
options = parse_vars(argv[2:])
setup_logging(config_uri)
settings = get_appsettings(config_uri, options=options)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+
+ engine = get_engine(settings)
+ dbmaker = get_dbmaker(engine)
+
+ dbsession = get_session(transaction.manager, dbmaker)
+
Base.metadata.create_all(engine)
+
with transaction.manager:
model = MyModel(name='one', value=1)
- DBSession.add(model)
+ dbsession.add(model)
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/favicon.ico b/docs/quick_tour/sqla_demo/sqla_demo/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/footerbg.png b/docs/quick_tour/sqla_demo/sqla_demo/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/headerbg.png b/docs/quick_tour/sqla_demo/sqla_demo/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css b/docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css
+++ /dev/null
@@ -1,8 +0,0 @@
-* html img,
-* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none",
-this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')",
-this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''),
-this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')",
-this.runtimeStyle.backgroundImage = "none")),this.pngSet=true)
-);}
-#wrap{display:table;height:100%}
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/middlebg.png b/docs/quick_tour/sqla_demo/sqla_demo/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pylons.css b/docs/quick_tour/sqla_demo/sqla_demo/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/pylons.css
+++ /dev/null
@@ -1,372 +0,0 @@
-html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td
-{
- margin: 0;
- padding: 0;
- border: 0;
- outline: 0;
- font-size: 100%; /* 16px */
- vertical-align: baseline;
- background: transparent;
-}
-
-body
-{
- line-height: 1;
-}
-
-ol, ul
-{
- list-style: none;
-}
-
-blockquote, q
-{
- quotes: none;
-}
-
-blockquote:before, blockquote:after, q:before, q:after
-{
- content: '';
- content: none;
-}
-
-:focus
-{
- outline: 0;
-}
-
-ins
-{
- text-decoration: none;
-}
-
-del
-{
- text-decoration: line-through;
-}
-
-table
-{
- border-collapse: collapse;
- border-spacing: 0;
-}
-
-sub
-{
- vertical-align: sub;
- font-size: smaller;
- line-height: normal;
-}
-
-sup
-{
- vertical-align: super;
- font-size: smaller;
- line-height: normal;
-}
-
-ul, menu, dir
-{
- display: block;
- list-style-type: disc;
- margin: 1em 0;
- padding-left: 40px;
-}
-
-ol
-{
- display: block;
- list-style-type: decimal-leading-zero;
- margin: 1em 0;
- padding-left: 40px;
-}
-
-li
-{
- display: list-item;
-}
-
-ul ul, ul ol, ul dir, ul menu, ul dl, ol ul, ol ol, ol dir, ol menu, ol dl, dir ul, dir ol, dir dir, dir menu, dir dl, menu ul, menu ol, menu dir, menu menu, menu dl, dl ul, dl ol, dl dir, dl menu, dl dl
-{
- margin-top: 0;
- margin-bottom: 0;
-}
-
-ol ul, ul ul, menu ul, dir ul, ol menu, ul menu, menu menu, dir menu, ol dir, ul dir, menu dir, dir dir
-{
- list-style-type: circle;
-}
-
-ol ol ul, ol ul ul, ol menu ul, ol dir ul, ol ol menu, ol ul menu, ol menu menu, ol dir menu, ol ol dir, ol ul dir, ol menu dir, ol dir dir, ul ol ul, ul ul ul, ul menu ul, ul dir ul, ul ol menu, ul ul menu, ul menu menu, ul dir menu, ul ol dir, ul ul dir, ul menu dir, ul dir dir, menu ol ul, menu ul ul, menu menu ul, menu dir ul, menu ol menu, menu ul menu, menu menu menu, menu dir menu, menu ol dir, menu ul dir, menu menu dir, menu dir dir, dir ol ul, dir ul ul, dir menu ul, dir dir ul, dir ol menu, dir ul menu, dir menu menu, dir dir menu, dir ol dir, dir ul dir, dir menu dir, dir dir dir
-{
- list-style-type: square;
-}
-
-.hidden
-{
- display: none;
-}
-
-p
-{
- line-height: 1.5em;
-}
-
-h1
-{
- font-size: 1.75em;
- line-height: 1.7em;
- font-family: helvetica, verdana;
-}
-
-h2
-{
- font-size: 1.5em;
- line-height: 1.7em;
- font-family: helvetica, verdana;
-}
-
-h3
-{
- font-size: 1.25em;
- line-height: 1.7em;
- font-family: helvetica, verdana;
-}
-
-h4
-{
- font-size: 1em;
- line-height: 1.7em;
- font-family: helvetica, verdana;
-}
-
-html, body
-{
- width: 100%;
- height: 100%;
-}
-
-body
-{
- margin: 0;
- padding: 0;
- background-color: #fff;
- position: relative;
- font: 16px/24px NobileRegular, "Lucida Grande", Lucida, Verdana, sans-serif;
-}
-
-a
-{
- color: #1b61d6;
- text-decoration: none;
-}
-
-a:hover
-{
- color: #e88f00;
- text-decoration: underline;
-}
-
-body h1, body h2, body h3, body h4, body h5, body h6
-{
- font-family: NeutonRegular, "Lucida Grande", Lucida, Verdana, sans-serif;
- font-weight: 400;
- color: #373839;
- font-style: normal;
-}
-
-#wrap
-{
- min-height: 100%;
-}
-
-#header, #footer
-{
- width: 100%;
- color: #fff;
- height: 40px;
- position: absolute;
- text-align: center;
- line-height: 40px;
- overflow: hidden;
- font-size: 12px;
- vertical-align: middle;
-}
-
-#header
-{
- background: #000;
- top: 0;
- font-size: 14px;
-}
-
-#footer
-{
- bottom: 0;
- background: #000 url(footerbg.png) repeat-x 0 top;
- position: relative;
- margin-top: -40px;
- clear: both;
-}
-
-.header, .footer
-{
- width: 750px;
- margin-right: auto;
- margin-left: auto;
-}
-
-.wrapper
-{
- width: 100%;
-}
-
-#top, #top-small, #bottom
-{
- width: 100%;
-}
-
-#top
-{
- color: #000;
- height: 230px;
- background: #fff url(headerbg.png) repeat-x 0 top;
- position: relative;
-}
-
-#top-small
-{
- color: #000;
- height: 60px;
- background: #fff url(headerbg.png) repeat-x 0 top;
- position: relative;
-}
-
-#bottom
-{
- color: #222;
- background-color: #fff;
-}
-
-.top, .top-small, .middle, .bottom
-{
- width: 750px;
- margin-right: auto;
- margin-left: auto;
-}
-
-.top
-{
- padding-top: 40px;
-}
-
-.top-small
-{
- padding-top: 10px;
-}
-
-#middle
-{
- width: 100%;
- height: 100px;
- background: url(middlebg.png) repeat-x;
- border-top: 2px solid #fff;
- border-bottom: 2px solid #b2b2b2;
-}
-
-.app-welcome
-{
- margin-top: 25px;
-}
-
-.app-name
-{
- color: #000;
- font-weight: 700;
-}
-
-.bottom
-{
- padding-top: 50px;
-}
-
-#left
-{
- width: 350px;
- float: left;
- padding-right: 25px;
-}
-
-#right
-{
- width: 350px;
- float: right;
- padding-left: 25px;
-}
-
-.align-left
-{
- text-align: left;
-}
-
-.align-right
-{
- text-align: right;
-}
-
-.align-center
-{
- text-align: center;
-}
-
-ul.links
-{
- margin: 0;
- padding: 0;
-}
-
-ul.links li
-{
- list-style-type: none;
- font-size: 14px;
-}
-
-form
-{
- border-style: none;
-}
-
-fieldset
-{
- border-style: none;
-}
-
-input
-{
- color: #222;
- border: 1px solid #ccc;
- font-family: sans-serif;
- font-size: 12px;
- line-height: 16px;
-}
-
-input[type=text], input[type=password]
-{
- width: 205px;
-}
-
-input[type=submit]
-{
- background-color: #ddd;
- font-weight: 700;
-}
-
-/*Opera Fix*/
-body:before
-{
- content: "";
- height: 100%;
- float: left;
- width: 0;
- margin-top: -32767px;
-}
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-small.png b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css
deleted file mode 100644
index 0d25de5b6..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/transparent.gif b/docs/quick_tour/sqla_demo/sqla_demo/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/static/transparent.gif
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt b/docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2
index c9b0cec21..76a098122 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt
+++ b/docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2
@@ -1,12 +1,12 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('sqla_demo:static/pyramid-16x16.png')}}">
<title>Alchemy Scaffold for The Pyramid Web Framework</title>
@@ -14,7 +14,7 @@
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('sqla_demo:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -29,19 +29,19 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('sqla_demo:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7.dev0</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2 b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2
new file mode 100644
index 000000000..bb622bf5a
--- /dev/null
+++ b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7.dev0</span>.</p>
+</div>
+{% endblock content %}
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt
deleted file mode 100644
index 99df4a8b7..000000000
--- a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt
+++ /dev/null
@@ -1,67 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('sqla_demo:static/pyramid-16x16.png')}">
-
- <title>Alchemy Scaffold for The Pyramid Web Framework</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('sqla_demo:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
-
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('sqla_demo:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.6</span>.</p>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="links">
- <ul>
- <li class="current-version">Generated by v1.6</li>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.6-branch/">Docs</a></li>
- <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
- <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
- <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
- </ul>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/tests.py b/docs/quick_tour/sqla_demo/sqla_demo/tests.py
index be288d580..b6b6fdf4d 100644
--- a/docs/quick_tour/sqla_demo/sqla_demo/tests.py
+++ b/docs/quick_tour/sqla_demo/sqla_demo/tests.py
@@ -3,53 +3,63 @@ import transaction
from pyramid import testing
-from .models import DBSession
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
-class TestMyViewSuccessCondition(unittest.TestCase):
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- from sqlalchemy import create_engine
- engine = create_engine('sqlite://')
- from .models import (
- Base,
- MyModel,
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models.meta')
+ settings = self.config.get_settings()
+
+ from .models.meta import (
+ get_session,
+ get_engine,
+ get_dbmaker,
)
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = MyModel(name='one', value=55)
- DBSession.add(model)
+
+ self.engine = get_engine(settings)
+ dbmaker = get_dbmaker(self.engine)
+
+ self.session = get_session(transaction.manager, dbmaker)
+
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- DBSession.remove()
+ from .models.meta import Base
+
testing.tearDown()
+ transaction.abort()
+ Base.metadata.create_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
+
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models.mymodel import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
def test_passing_view(self):
- from .views import my_view
- request = testing.DummyRequest()
- info = my_view(request)
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
self.assertEqual(info['one'].name, 'one')
self.assertEqual(info['project'], 'sqla_demo')
-class TestMyViewFailureCondition(unittest.TestCase):
- def setUp(self):
- self.config = testing.setUp()
- from sqlalchemy import create_engine
- engine = create_engine('sqlite://')
- from .models import (
- Base,
- MyModel,
- )
- DBSession.configure(bind=engine)
-
- def tearDown(self):
- DBSession.remove()
- testing.tearDown()
+class TestMyViewFailureCondition(BaseTest):
def test_failing_view(self):
- from .views import my_view
- request = testing.DummyRequest()
- info = my_view(request)
- self.assertEqual(info.status_int, 500) \ No newline at end of file
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/views.py b/docs/quick_tour/sqla_demo/sqla_demo/views/default.py
index 964f76441..e5e70cf9d 100644
--- a/docs/quick_tour/sqla_demo/sqla_demo/views.py
+++ b/docs/quick_tour/sqla_demo/sqla_demo/views/default.py
@@ -3,22 +3,22 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from .models import (
- DBSession,
- MyModel,
- )
+from ..models.mymodel import MyModel
-@view_config(route_name='home', renderer='templates/mytemplate.pt')
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(MyModel)
+ # Start Sphinx Include
+ one = query.filter(MyModel.name == 'one').first()
+ # End Sphinx Include
except DBAPIError:
- return Response(conn_err_msg, content_type='text/plain', status_int=500)
+ return Response(db_err_msg, content_type='text/plain', status_int=500)
return {'one': one, 'project': 'sqla_demo'}
-conn_err_msg = """\
+db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
@@ -33,4 +33,3 @@ might be caused by one of the following things:
After you fix the problem, please restart the Pyramid application to
try it again.
"""
-
diff --git a/docs/quick_tutorial/authentication.rst b/docs/quick_tutorial/authentication.rst
index 7fd8173d4..acff97f3b 100644
--- a/docs/quick_tutorial/authentication.rst
+++ b/docs/quick_tutorial/authentication.rst
@@ -1,29 +1,30 @@
+.. _qtut_authentication:
+
==============================
20: Logins With Authentication
==============================
-Login views that authenticate a username/password against a list of
-users.
+Login views that authenticate a username and 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 <security_chapter>`
-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.
+Most web applications have URLs that allow people to add/edit/delete content
+via a web browser. Time to add :ref:`security <security_chapter>` 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 storage.
+
+In the next step we will introduce protection of resources with authorization
+security statements.
-In the next step we will introduce protection resources with
-authorization security statements.
Objectives
==========
-- Introduce the Pyramid concepts of authentication
+- Introduce the Pyramid concepts of authentication.
-- Create login/logout views
+- Create login and logout views.
Steps
=====
@@ -33,25 +34,23 @@ Steps
.. code-block:: bash
$ cd ..; cp -r view_classes authentication; cd authentication
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. Put the security hash in the ``authentication/development.ini``
- configuration file as ``tutorial.secret`` instead of putting it in
- the code:
+ 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``:
+#. 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*:
+#. Create an ``authentication/tutorial/security.py`` module that can find our
+ user information by providing an *authentication policy callback*:
.. literalinclude:: authentication/tutorial/security.py
:linenos:
@@ -67,7 +66,7 @@ Steps
:language: html
:linenos:
-#. Provide a login/logout box in ``authentication/tutorial/home.pt``
+#. Provide a login/logout box in ``authentication/tutorial/home.pt``:
.. literalinclude:: authentication/tutorial/home.pt
:language: html
@@ -93,39 +92,37 @@ Steps
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 <authentication_module>`
-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
+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
+<authentication_module>` 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`` request, it
+returned a login form. When reached via ``POST``, it processed the submitted
+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?
-#. Once I am logged in, does any user-centric information get jammed
- onto each request? Use ``import pdb; pdb.set_trace()`` to answer
- this.
+#. 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:: See also :ref:`security_chapter`,
:ref:`AuthTktAuthenticationPolicy <authentication_module>`.
diff --git a/docs/quick_tutorial/authorization.rst b/docs/quick_tutorial/authorization.rst
index 855043f7f..58c1d2582 100644
--- a/docs/quick_tutorial/authorization.rst
+++ b/docs/quick_tutorial/authorization.rst
@@ -1,34 +1,38 @@
+.. _qtut_authorization:
+
===========================================
21: Protecting Resources With Authorization
===========================================
-Assign security statements to resources describing the permissions
-required to perform an operation.
+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.
+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)
+- 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
+- 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
+- Assign security statements to our root resource.
-- Add a permissions predicate on a view
+- Add a permissions predicate on a view.
+
+- Provide a :term:`Forbidden view` to handle visiting a URL without adequate
+ permissions.
-- Provide a :term:`Forbidden view` to handle visiting a URL without
- adequate permissions
Steps
=====
@@ -38,16 +42,15 @@ Steps
.. code-block:: bash
$ cd ..; cp -r authentication authorization; cd authorization
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
-#. Start by changing ``authorization/tutorial/__init__.py`` to
- specify a root factory to the :term:`configurator`:
+#. Start by changing ``authorization/tutorial/__init__.py`` to specify a root
+ factory to the :term:`configurator`:
.. literalinclude:: authorization/tutorial/__init__.py
:linenos:
-#. That means we need to implement
- ``authorization/tutorial/resources.py``
+#. That means we need to implement ``authorization/tutorial/resources.py``:
.. literalinclude:: authorization/tutorial/resources.py
:linenos:
@@ -68,48 +71,47 @@ Steps
#. 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.
+#. 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``)
+- A view can require a *permission* (``edit``).
-- The context for our view (the ``Root``) has an access control list
- (ACL)
+- 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*
+- 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``)
+- The registered ``groupfinder`` answers whether a particular user (``editor``)
+ has a particular group (``group:editors``).
-In summary: ``hello`` wants ``edit`` permission, ``Root`` says
+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.
+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 ``/howdy``, 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``.
-If you are not logged in and visit ``/howdy``, 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
+Extra credit
============
-#. Do I have to put a ``renderer`` in my ``@forbidden_view_config``
- decorator?
+#. Do I have to put a ``renderer`` in my ``@forbidden_view_config`` decorator?
#. Perhaps you would like the 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?
+#. 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?
+#. 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/conf.py b/docs/quick_tutorial/conf.py
deleted file mode 100644
index 47b8fae41..000000000
--- a/docs/quick_tutorial/conf.py
+++ /dev/null
@@ -1,281 +0,0 @@
-# -*- 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
-# "<project> v<release> 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 <link> 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
index 19dfd066d..c8d87c180 100644
--- a/docs/quick_tutorial/databases.rst
+++ b/docs/quick_tutorial/databases.rst
@@ -4,37 +4,39 @@
19: Databases Using SQLAlchemy
==============================
-Store/retrieve data using the SQLAlchemy ORM atop the SQLite database.
+Store and 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 <sqla:index_toplevel>` project and its
-:ref:`object-relational mapper (ORM) <sqla:ormtutorial_toplevel>`
-as a convenient, Pythonic way to interface to databases.
+Our Pyramid-based wiki application now needs database-backed storage of pages.
+This frequently means an SQL database. The Pyramid community strongly supports
+the :ref:`SQLAlchemy <sqla:index_toplevel>` project and its
+:ref:`object-relational mapper (ORM) <sqla:ormtutorial_toplevel>` 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.
+In this step we hook up SQLAlchemy to a SQLite database table, providing
+storage and retrieval for the wiki pages 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.
+ The ``alchemy`` scaffold is really helpful for getting an 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
+- Store pages in SQLite by using SQLAlchemy models.
-- Use SQLAlchemy queries to list/add/view/edit pages
+- 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.
-- Provide a database-initialize command by writing a Pyramid *console
- script* which can be run from the command line
Steps
=====
@@ -45,31 +47,31 @@ Steps
$ 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:
+#. 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 ``$VENV/bin/python setup.py develop`` as we
- will change it later.
+ We aren't yet doing ``$VENV/bin/pip install -e .`` as we will change it
+ later.
-#. Our configuration file at ``databases/development.ini`` wires
- together some new pieces:
+#. 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``:
+#. 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:
+#. Make a command-line script at ``databases/tutorial/initialize_db.py`` to
+ initialize the database:
.. literalinclude:: databases/tutorial/initialize_db.py
:linenos:
@@ -78,7 +80,7 @@ Steps
.. code-block:: bash
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. The script references some models in ``databases/tutorial/models.py``:
@@ -90,51 +92,49 @@ Steps
.. code-block:: bash
$ $VENV/bin/initialize_tutorial_db development.ini
- 2015-06-01 11:22:52,650 INFO [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
- 2015-06-01 11:22:52,650 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2015-06-01 11:22:52,651 INFO [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
- 2015-06-01 11:22:52,651 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2015-06-01 11:22:52,652 INFO [sqlalchemy.engine.base.Engine][MainThread] PRAGMA table_info("wikipages")
- 2015-06-01 11:22:52,652 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2015-06-01 11:22:52,653 INFO [sqlalchemy.engine.base.Engine][MainThread]
+
+ 2016-04-16 13:01:33,055 INFO [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2016-04-16 13:01:33,055 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
+ 2016-04-16 13:01:33,056 INFO [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2016-04-16 13:01:33,056 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
+ 2016-04-16 13:01:33,057 INFO [sqlalchemy.engine.base.Engine][MainThread] PRAGMA table_info("wikipages")
+ 2016-04-16 13:01:33,057 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
+ 2016-04-16 13:01:33,058 INFO [sqlalchemy.engine.base.Engine][MainThread]
CREATE TABLE wikipages (
- uid INTEGER NOT NULL,
- title TEXT,
- body TEXT,
- PRIMARY KEY (uid),
- UNIQUE (title)
+ uid INTEGER NOT NULL,
+ title TEXT,
+ body TEXT,
+ PRIMARY KEY (uid),
+ UNIQUE (title)
)
- 2015-06-01 11:22:52,653 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2015-06-01 11:22:52,655 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
- 2015-06-01 11:22:52,658 INFO [sqlalchemy.engine.base.Engine][MainThread] BEGIN (implicit)
- 2015-06-01 11:22:52,659 INFO [sqlalchemy.engine.base.Engine][MainThread] INSERT INTO wikipages (title, body) VALUES (?, ?)
- 2015-06-01 11:22:52,659 INFO [sqlalchemy.engine.base.Engine][MainThread] ('Root', '<p>Root</p>')
- 2015-06-01 11:22:52,659 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
+ 2016-04-16 13:01:33,058 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
+ 2016-04-16 13:01:33,059 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
+ 2016-04-16 13:01:33,062 INFO [sqlalchemy.engine.base.Engine][MainThread] BEGIN (implicit)
+ 2016-04-16 13:01:33,062 INFO [sqlalchemy.engine.base.Engine][MainThread] INSERT INTO wikipages (title, body) VALUES (?, ?)
+ 2016-04-16 13:01:33,063 INFO [sqlalchemy.engine.base.Engine][MainThread] ('Root', '<p>Root</p>')
+ 2016-04-16 13:01:33,063 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT
-#. With our data now driven by SQLAlchemy queries, we need to update
- our ``databases/tutorial/views.py``:
+#. With our data now driven by SQLAlchemy queries, we need to update our
+ ``databases/tutorial/views.py``:
.. literalinclude:: databases/tutorial/views.py
:linenos:
-#. Our tests in ``databases/tutorial/tests.py`` changed to include
- SQLAlchemy bootstrapping:
+#. 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``:
+#. Run the tests in your package using ``py.test``:
- .. code-block:: bash
-
- $ $VENV/bin/nosetests tutorial
- ..
- -----------------------------------------------------------------
- Ran 2 tests in 1.141s
+ .. code-block:: bash
- OK
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ..
+ 2 passed in 1.41 seconds
#. Run your Pyramid application with:
@@ -144,57 +144,55 @@ Steps
#. 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?
+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
-application either as middleware or a Pyramid "tween". Then,
-just before you return the response, all transaction-aware parts of
-your application are executed.
+Specifically, you can install a transaction manager into your 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.
+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.
+``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``.
+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 virtual environment'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 of 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.
+dictionary-of-dictionaries data with proper database support: list the rows,
+add a row, edit a row, and delete a row.
+
-Extra Credit
+Extra credit
============
-#. Why all this code? Why can't I just type 2 lines and have magic ensue?
+#. Why all this code? Why can't I just type two lines and have magic ensue?
#. Give a try at a button that deletes a wiki page.
diff --git a/docs/quick_tutorial/databases/sqltutorial.sqlite b/docs/quick_tutorial/databases/sqltutorial.sqlite
deleted file mode 100644
index b8bd856fd..000000000
--- a/docs/quick_tutorial/databases/sqltutorial.sqlite
+++ /dev/null
Binary files differ
diff --git a/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt
index 01955ef72..d1fea0d7f 100644
--- a/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt
+++ b/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt
@@ -4,12 +4,10 @@
<title>WikiPage: Add/Edit</title>
<tal:block tal:repeat="reqt view.reqts['css']">
<link rel="stylesheet" type="text/css"
- href="${request.static_url(reqt)}">
+ href="${request.static_url('deform:static/' + reqt)}"/>
</tal:block>
- <script type="text/javascript"
- src="${request.static_url('deform:static/scripts/jquery-2.0.3.min.js')}"></script>
<tal:block tal:repeat="reqt view.reqts['js']">
- <script src="${request.static_url(reqt)}"
+ <script src="${request.static_url('deform:static/' + reqt)}"
type="text/javascript"></script>
</tal:block>
</head>
diff --git a/docs/quick_tutorial/debugtoolbar.rst b/docs/quick_tutorial/debugtoolbar.rst
index f11abc493..aaf904390 100644
--- a/docs/quick_tutorial/debugtoolbar.rst
+++ b/docs/quick_tutorial/debugtoolbar.rst
@@ -4,40 +4,42 @@
04: Easier Development with ``debugtoolbar``
============================================
-Error-handling and introspection using the ``pyramid_debugtoolbar``
-add-on.
+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.
+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.
-``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
+- Install and enable the toolbar to help during development.
+
+- Explain Pyramid add-ons.
-- Explain Pyramid add-ons
+- Show how an add-on gets configured into your application.
-- 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:
+#. First we copy the results of the previous step, as well as install the
+ ``pyramid_debugtoolbar`` package:
.. code-block:: bash
$ cd ..; cp -r ini debugtoolbar; cd debugtoolbar
- $ $VENV/bin/python setup.py develop
- $ $VENV/bin/easy_install pyramid_debugtoolbar
+ $ $VENV/bin/pip install -e .
+ $ $VENV/bin/pip install pyramid_debugtoolbar
#. Our ``debugtoolbar/development.ini`` gets a configuration entry for
``pyramid.includes``:
@@ -55,6 +57,7 @@ Steps
#. Open http://localhost:6543/ in your browser. See the handy
toolbar on the right.
+
Analysis
========
@@ -67,16 +70,16 @@ 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.
+via our ``development.ini`` using ``pyramid.includes``. We use this to load the
+configuration for the debugtoolbar.
You'll now see an attractive button on the right side of your browser, which
-you may click to provide introspective access to debugging information in a
-new browser tab. 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, there's 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.)
+you may click to provide introspective access to debugging information in a new
+rowser tab. 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,
+there's 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 injects a small amount of HTML/CSS into your app just
before the closing ``</body>`` tag in order to display itself. If you start to
@@ -86,13 +89,14 @@ temporarily.
.. seealso:: See also :ref:`pyramid_debugtoolbar <toolbar:overview>`.
+
Extra Credit
============
#. Why don't we add ``pyramid_debugtoolbar`` to the list of
``install_requires`` dependencies in ``debugtoolbar/setup.py``?
-#. Introduce a bug into your application: Change:
+#. Introduce a bug into your application. Change:
.. code-block:: python
@@ -106,7 +110,7 @@ Extra Credit
def hello_world(request):
return xResponse('<body><h1>Hello World!</h1></body>')
- Save, and visit http://localhost:6543/ again. Notice the nice
- traceback display. On the lowest line, click the "screen" icon to the
- right, and try typing the variable names ``request`` and ``Response``.
- What else can you discover?
+ Save, and visit http://localhost:6543/ again. Notice the nice traceback
+ display. On the lowest line, click the "screen" icon to the right, and try
+ typing the variable names ``request`` and ``Response``. What else can you
+ discover?
diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst
index f81b88fc2..6b29833bd 100644
--- a/docs/quick_tutorial/forms.rst
+++ b/docs/quick_tutorial/forms.rst
@@ -6,30 +6,30 @@
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.
+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 <deform:overview>`
-is one such library. In this step, we introduce Deform for our
-forms and validation. This also gives us :ref:`Colander <colander:overview>`
+:ref:`Deform <deform:overview>` is one such library. In this step, we introduce
+Deform for our forms. This also gives us :ref:`Colander <colander:overview>`
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.
+Deform uses styling from Twitter Bootstrap and advanced widgets from popular
+JavaScript projects.
+
Objectives
==========
-- Make a schema using Colander, the companion to Deform
+- Make a schema using Colander, the companion to Deform.
+
+- Create a form with Deform and change our views to handle validation.
-- Create a form with Deform and change our views to handle validation
Steps
=====
@@ -40,8 +40,8 @@ Steps
$ 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:
+#. 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:
@@ -50,23 +50,21 @@ Steps
.. code-block:: bash
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
-#. Register a static view in ``forms/tutorial/__init__.py`` for
- Deform's CSS/JS etc. as well as our demo wikipage scenario's
- views:
+#. Register a static view in ``forms/tutorial/__init__.py`` for Deform's CSS,
+ JavaScript, etc., as well as our demo wiki page'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``:
+#. 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``:
+#. A template for the top of the "wiki" in ``forms/tutorial/wiki_view.pt``:
.. literalinclude:: forms/tutorial/wiki_view.pt
:language: html
@@ -79,13 +77,21 @@ Steps
:language: html
:linenos:
-#. Finally, a template at ``forms/tutorial/wikipage_view.pt``
- for viewing a wiki page:
+#. Finally, a template at ``forms/tutorial/wikipage_view.pt`` for viewing a
+ wiki page:
.. literalinclude:: forms/tutorial/wikipage_view.pt
:language: html
:linenos:
+#. Run the tests:
+
+ .. code-block:: bash
+
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ..
+ 2 passed in 0.45 seconds
+
#. Run your Pyramid application with:
.. code-block:: bash
@@ -98,51 +104,50 @@ Steps
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 packages,
-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
+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 packages, 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 JavaScript just
+mentioned. Deform has a :term:`resource registry` which allows widgets to
+specify which JavaScript 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 URL whether you are walking up
+to it for 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
``<input name="submit">``.
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 ``GET``, skip over and just return the form.
+
+- If you are doing a ``POST``, validate the form contents.
-- 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 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``.
-- 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.
-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
+Extra credit
============
-#. Give a try at a button that goes to a delete view for a
- particular wiki page.
+#. Give a try at a button that goes to a delete view for a particular wiki
+ page.
diff --git a/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt
index 547465018..d1fea0d7f 100644
--- a/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt
+++ b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt
@@ -4,12 +4,10 @@
<title>WikiPage: Add/Edit</title>
<tal:block tal:repeat="reqt view.reqts['css']">
<link rel="stylesheet" type="text/css"
- href="${request.static_url(reqt)}"/>
+ href="${request.static_url('deform:static/' + reqt)}"/>
</tal:block>
- <script src="${request.static_url('deform:static/scripts/jquery-2.0.3.min.js')}"
- type="text/javascript"></script>
<tal:block tal:repeat="reqt view.reqts['js']">
- <script src="${request.static_url(reqt)}"
+ <script src="${request.static_url('deform:static/' + reqt)}"
type="text/javascript"></script>
</tal:block>
</head>
diff --git a/docs/quick_tutorial/functional_testing.rst b/docs/quick_tutorial/functional_testing.rst
index 6f1544e79..33793578a 100644
--- a/docs/quick_tutorial/functional_testing.rst
+++ b/docs/quick_tutorial/functional_testing.rst
@@ -6,36 +6,39 @@
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.
+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 <http://docs.pylonsproject.org/projects/webtest/en/latest/>`_ 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.
-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
+- 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:
+#. First we copy the results of the previous step, as well as install the
+ ``webtest`` package:
.. code-block:: bash
$ cd ..; cp -r unit_testing functional_testing; cd functional_testing
- $ $VENV/bin/python setup.py develop
- $ $VENV/bin/easy_install webtest
+ $ $VENV/bin/pip install -e .
+ $ $VENV/bin/pip install webtest
#. Let's extend ``functional_testing/tutorial/tests.py`` to include a
functional test:
@@ -43,31 +46,28 @@ Steps
.. literalinclude:: functional_testing/tutorial/tests.py
:linenos:
- Be sure this file is not executable, or ``nosetests`` may not
- include your tests.
+ Be sure this file is not executable, or ``pytest`` may not include your
+ tests.
#. Now run the tests:
.. code-block:: bash
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ..
+ 2 passed in 0.25 seconds
- $ $VENV/bin/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.
+We now have the end-to-end testing we were looking for. WebTest lets us simply
+extend our existing ``pytest``-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
+Extra credit
============
#. Why do our functional tests use ``b''``?
diff --git a/docs/quick_tutorial/hello_world.rst b/docs/quick_tutorial/hello_world.rst
index 4ae80ca87..4e35da7bb 100644
--- a/docs/quick_tutorial/hello_world.rst
+++ b/docs/quick_tutorial/hello_world.rst
@@ -4,40 +4,40 @@
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.
+What's the simplest way to get started in Pyramid? A single-file module. No
+Python packages, no ``pip install -e .``, 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*.
+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.
-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.
-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.
-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
+- Get a running Pyramid web application, as simply as possible.
+
+- Use that as a well-understood base for adding each unit of complexity.
-- Use that as a well-understood base for adding each unit of complexity
+- Initial exposure to WSGI apps, requests, views, and responses.
-- Initial exposure to WSGI apps, requests, views, and responses
Steps
=====
@@ -64,30 +64,29 @@ Steps
#. Open http://localhost:6543/ in your browser.
+
Analysis
========
-New to Python web programming? If so, some lines in module merit
+New to Python web programming? If so, some lines in the module merit
explanation:
-#. *Line 11*. The ``if __name__ == '__main__':`` is Python's way of
- saying "Start here when running from the command line", rather than
- when this module is imported.
+#. *Line 11*. The ``if __name__ == '__main__':`` is Python's way of saying,
+ "Start here when running from the command line", rather than when this
+ module is imported.
+
+#. *Lines 12-14*. Use Pyramid's :term:`configurator` to connect :term:`view`
+ code to a particular URL :term:`route`.
-#. *Lines 12-14*. Use Pyramid's :term:`configurator` to connect
- :term:`view` code to a particular URL :term:`route`.
+#. *Lines 6-8*. Implement the view code that generates the :term:`response`.
-#. *Lines 6-8*. Implement the view code that generates the
- :term:`response`.
+#. *Lines 15-17*. Publish a :term:`WSGI` app using an HTTP server.
-#. *Lines 15-17*. Publish a :term:`WSGI` app using an HTTP
- server.
+As shown in this example, the :term:`configurator` plays a central role in
+Pyramid development. Building an application from loosely-coupled parts via
+:ref:`configuration_narr` is a central idea in Pyramid, one that we will
+revisit regularly in this *Quick Tutorial*.
-As shown in this example, the :term:`configurator` plays a
-central role in Pyramid development. Building an application from
-loosely-coupled parts via :ref:`configuration_narr` is a
-central idea in Pyramid, one that we will revisit regularly in this
-*Quick Tour*.
Extra Credit
============
@@ -106,9 +105,9 @@ Extra Credit
#. 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?
+#. Put something invalid, such as ``print xyz``, in the view function. Kill
+ your ``python app.py`` with ``ctrl-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?
+#. The ``GI`` in ``WSGI`` stands for "Gateway Interface". What web standard is
+ this modelled after?
diff --git a/docs/quick_tutorial/index.rst b/docs/quick_tutorial/index.rst
index 9373fe38a..29b4d8fb7 100644
--- a/docs/quick_tutorial/index.rst
+++ b/docs/quick_tutorial/index.rst
@@ -4,12 +4,12 @@
Quick Tutorial for Pyramid
==========================
-Pyramid is a web framework for Python 2 and 3. This tutorial gives a
-Python 3/2-compatible, high-level tour of the major features.
+Pyramid is a web framework for Python 2 and 3. This tutorial gives a Python
+3/2-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.
+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
========
diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst
index 36942c767..fba5ce29e 100644
--- a/docs/quick_tutorial/ini.rst
+++ b/docs/quick_tutorial/ini.rst
@@ -7,28 +7,30 @@
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 <configuration_narr>` 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 installing and providing
-"entry points" for Python projects. Pyramid uses an entry point to
-let a Pyramid application know where to find the WSGI app.
+Pyramid has a first-class concept of :ref:`configuration <configuration_narr>`
+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 installing and providing "entry
+points" for Python projects. Pyramid uses an entry point to let a Pyramid
+application know where to find the WSGI app.
+
Objectives
==========
-- Modify our ``setup.py`` to have an entry point telling Pyramid the
- location of the WSGI app
+- Modify our ``setup.py`` to have an entry point telling Pyramid the location
+ of the WSGI app.
+
+- Create an application driven by an ``.ini`` file.
-- Create an application driven by a ``.ini`` file
+- Start the application with Pyramid's ``pserve`` command.
-- Startup the application with Pyramid's ``pserve`` command
+- Move code into the package's ``__init__.py``.
-- Move code into the package's ``__init__.py``
Steps
=====
@@ -39,18 +41,18 @@ Steps
$ cd ..; cp -r package ini; cd ini
-#. Our ``ini/setup.py`` needs a setuptools "entry point" in the
- ``setup()`` function:
+#. 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``:
+#. We can now install our project, thus generating (or re-generating) an "egg"
+ at ``ini/tutorial.egg-info``:
.. code-block:: bash
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. Let's make a file ``ini/development.ini`` for our configuration:
@@ -58,8 +60,8 @@ Steps
:language: ini
:linenos:
-#. We can refactor our startup code from the previous step's ``app.py``
- into ``ini/tutorial/__init__.py``:
+#. We can refactor our startup code from the previous step's ``app.py`` into
+ ``ini/tutorial/__init__.py``:
.. literalinclude:: ini/tutorial/__init__.py
:linenos:
@@ -81,27 +83,26 @@ Steps
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
+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 <startup_chapter>`:
-- ``pserve`` looks for ``[app:main]`` and finds ``use = egg:tutorial``
+- ``pserve`` looks for ``[app:main]`` and finds ``use = egg:tutorial``.
-- The projects's ``setup.py`` has defined an "entry point" (lines 9-12)
- for the project "main" entry point of ``tutorial:main``
+- The projects's ``setup.py`` has defined an "entry point" (lines 9-12) for the
+ project's "main" entry point of ``tutorial:main``.
-- The ``tutorial`` package's ``__init__`` has a ``main`` function
+- The ``tutorial`` package's ``__init__`` has a ``main`` function.
-- This function is invoked, with the values from certain ``.ini``
- sections passed in
+- This function is invoked, with the values from certain ``.ini`` sections
+ passed in.
The ``.ini`` file is also used for two other functions:
-- *Configuring the WSGI server*. ``[server:main]`` wires up the choice of
- which WSGI *server* for your WSGI *application*. In this case, we are using
- ``wsgiref`` bundled in the Python library. It also wires up the *port
- number*: ``port = 6543`` tells ``wsgiref`` to listen on port 6543.
+- *Configuring the WSGI server*. ``[server:main]`` wires up the choice of which
+ WSGI *server* for your WSGI *application*. In this case, we are using
+ ``wsgiref`` bundled in the Python library. It also wires up the *port
+ number*: ``port = 6543`` tells ``wsgiref`` to listen on port 6543.
- *Configuring Python logging*. Pyramid uses Python standard logging, which
needs a number of configuration values. The ``.ini`` serves this function.
@@ -109,27 +110,27 @@ The ``.ini`` file is also used for two other functions:
request.
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``.
+``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.
-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?
+#. 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?
+#. 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 declared ``tutorial:main`` function. Why not?
+#. The entry point in ``setup.py`` didn't mention ``__init__.py`` when it
+ declared ``tutorial:main`` function. Why not?
#. What is the purpose of ``**settings``? What does the ``**`` signify?
@@ -139,4 +140,3 @@ Extra Credit
:ref:`what_is_this_pserve_thing`,
:ref:`environment_chapter`,
:ref:`paste_chapter`
-
diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst
index 2121803f9..2fc68827b 100644
--- a/docs/quick_tutorial/jinja2.rst
+++ b/docs/quick_tutorial/jinja2.rst
@@ -4,33 +4,34 @@
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 modeled 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.
+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 modeled
+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
+- Show Pyramid's support for different templating systems.
+
+- Learn about installing Pyramid add-ons.
-- Learn about installing Pyramid add-ons
Steps
=====
-#. In this step let's start by copying the ``view_class`` step's
- directory, and then installing the ``pyramid_jinja2`` add-on.
+#. In this step let's start by copying the ``view_class`` step's directory,
+ and then installing the ``pyramid_jinja2`` add-on.
.. code-block:: bash
$ cd ..; cp -r view_classes jinja2; cd jinja2
- $ $VENV/bin/python setup.py develop
- $ $VENV/bin/easy_install pyramid_jinja2
+ $ $VENV/bin/pip install -e .
+ $ $VENV/bin/pip install pyramid_jinja2
-#. We need to include ``pyramid_jinja2`` in
- ``jinja2/tutorial/__init__.py``:
+#. We need to include ``pyramid_jinja2`` in ``jinja2/tutorial/__init__.py``:
.. literalinclude:: jinja2/tutorial/__init__.py
:linenos:
@@ -49,7 +50,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ....
+ 4 passed in 0.40 seconds
#. Run your Pyramid application with:
@@ -59,30 +62,30 @@ Steps
#. 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
+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
+virtual environment. 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 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.
+
-Extra Credit
+Extra credit
============
-#. Our project now depends on ``pyramid_jinja2``. We installed that
- dependency manually. What is another way we could have made the
- association?
+#. Our project now depends on ``pyramid_jinja2``. We installed that dependency
+ manually. What is another way we could have made the association?
#. We used ``config.include`` which is an imperative configuration to get the
- :term:`Configurator` to load ``pyramid_jinja2``'s configuration.
- What is another way could include it into the config?
+ :term:`Configurator` to load ``pyramid_jinja2``'s configuration. What is
+ another way could include it into the config?
-.. seealso:: `Jinja2 homepage <http://jinja.pocoo.org/>`_,
- and
+.. seealso:: `Jinja2 homepage <http://jinja.pocoo.org/>`_, and
:ref:`pyramid_jinja2 Overview <jinja2:overview>`
diff --git a/docs/quick_tutorial/json.rst b/docs/quick_tutorial/json.rst
index aa789d833..ff153d2b5 100644
--- a/docs/quick_tutorial/json.rst
+++ b/docs/quick_tutorial/json.rst
@@ -1,27 +1,28 @@
.. _qtut_json:
========================================
-14: Ajax Development With JSON Renderers
+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*.
+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.
+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.
-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
=====
@@ -31,22 +32,20 @@ Steps
.. code-block:: bash
$ cd ..; cp -r view_classes json; cd json
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
-#. We add a new route for ``hello_json`` in
- ``json/tutorial/__init__.py``:
+#. 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 in ``views.py``:
+#. Rather than implement a new view, we will "stack" another decorator on the
+ ``hello`` view in ``views.py``:
.. literalinclude:: json/tutorial/views.py
:linenos:
-#. We need a new functional test at the end of
- ``json/tutorial/tests.py``:
+#. We need a new functional test at the end of ``json/tutorial/tests.py``:
.. literalinclude:: json/tutorial/tests.py
:linenos:
@@ -55,7 +54,10 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ .....
+ 5 passed in 0.47 seconds
+
#. Run your Pyramid application with:
@@ -63,40 +65,39 @@ Steps
$ $VENV/bin/pserve development.ini --reload
-#. Open http://localhost:6543/howdy.json in your browser and you
- will see the resulting JSON response.
+#. 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.
+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:
+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
+- Adding a route to map ``/howdy.json`` to a route name.
-- Providing a ``@view_config`` that associated that route name with an
- existing view
+- 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
+- *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.
+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 implementations.
-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.
+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:`views_which_use_a_renderer`,
:ref:`json_renderer`, and
diff --git a/docs/quick_tutorial/logging.rst b/docs/quick_tutorial/logging.rst
index 5d29cd196..cbbf7860e 100644
--- a/docs/quick_tutorial/logging.rst
+++ b/docs/quick_tutorial/logging.rst
@@ -4,28 +4,30 @@
16: Collecting Application Info With Logging
============================================
-Capture debugging and error output from your web applications using
-standard Python 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*.
+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`` has 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.
-Fortunately Pyramid uses the normal Python approach to logging. The
-scaffold generated in your ``development.ini`` has 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
+- Inspect the configuration setup used for logging.
+
+- Add logging statements to your view code.
-- Add logging statements to your view code
Steps
=====
@@ -35,15 +37,15 @@ Steps
.. code-block:: bash
$ cd ..; cp -r view_classes logging; cd logging
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. Extend ``logging/tutorial/views.py`` to log a message:
.. literalinclude:: logging/tutorial/views.py
:linenos:
-#. Finally let's edit ``development.ini`` configuration file
- to enable logging for our Pyramid application:
+#. Finally let's edit ``development.ini`` configuration file to enable logging
+ for our Pyramid application:
.. literalinclude:: logging/development.ini
:language: ini
@@ -52,7 +54,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ....
+ 4 passed in 0.41 seconds
#. Run your Pyramid application with:
@@ -60,19 +64,21 @@ Steps
$ $VENV/bin/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.
+#. 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
========
-In our configuration file ``development.ini``, 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::
+In our configuration file ``development.ini``, our ``tutorial`` Python package
+is set up 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:
+
+.. code-block:: text
- 2013-08-09 10:42:42,968 DEBUG [tutorial.views][MainThread] In home view
+ 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.
diff --git a/docs/quick_tutorial/more_view_classes.rst b/docs/quick_tutorial/more_view_classes.rst
index afbb7cc3a..30234ea2e 100644
--- a/docs/quick_tutorial/more_view_classes.rst
+++ b/docs/quick_tutorial/more_view_classes.rst
@@ -6,48 +6,49 @@
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.
+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 a ``__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.
-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.
+At first, our views were 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
+<class_as_view>` makes sense:
-At first, our views were 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 <class_as_view>` makes sense:
+- Group views.
-- Group views
+- Centralize some repetitive defaults.
-- Centralize some repetitive defaults
+- Share some state and helpers.
-- Share some state and helpers
+Pyramid views have :ref:`view predicates <view_configuration_parameters>` that
+determine which view is matched to a request, based on factors such as the
+request method, the form parameters, and so on. These predicates provide many
+axes of flexibility.
-Pyramid views have :ref:`view predicates <view_configuration_parameters>`
-that determine which view is matched to a request, based on factors
-such as the request method, the form parameters, etc. These predicates
-provide many axes of flexibility.
+The following shows a simple example with four operations: view a home page
+which leads to a form, save a change, and press the delete button.
-The following shows a simple example with four 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
+- Group related views into a view class.
-- Centralize configuration with class-level ``@view_defaults``
+- Centralize configuration with class-level ``@view_defaults``.
-- Dispatch one route/URL to multiple views based on request data
+- Dispatch one route/URL to multiple views based on request data.
+
+- Share states and logic between views and templates via the view class.
-- Share states and logic between views and templates via the view class
Steps
=====
@@ -57,7 +58,7 @@ Steps
.. code-block:: bash
$ cd ..; cp -r templating more_view_classes; cd more_view_classes
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. Our route in ``more_view_classes/tutorial/__init__.py`` needs some
replacement patterns:
@@ -71,8 +72,7 @@ Steps
.. literalinclude:: more_view_classes/tutorial/views.py
:linenos:
-#. Our primary view needs a template at
- ``more_view_classes/tutorial/home.pt``:
+#. Our primary view needs a template at ``more_view_classes/tutorial/home.pt``:
.. literalinclude:: more_view_classes/tutorial/home.pt
:language: html
@@ -105,12 +105,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
- .
- ----------------------------------------------------------------------
- Ran 2 tests in 0.248s
-
- OK
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ..
+ 2 passed in 0.40 seconds
#. Run your Pyramid application with:
@@ -118,29 +115,27 @@ Steps
$ $VENV/bin/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.
+#. 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:
+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.
+- 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
+- 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 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 ``<input type="submit" name="form.delete" value="Delete"/>``.
+- The fourth view is returned when clicking on a button such as ``<input
+ type="submit" name="form.delete" value="Delete"/>``.
In this step we show, using the following information as criteria, how to
decide which view to use:
@@ -149,49 +144,53 @@ decide which view to use:
- 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:
+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}``.
+These are then available both in the view methods and 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 hardcoded the URLs, such as:
-As a note, we made a switch in our templates on how we generate URLs.
-We previously hardcode the URLs, such as::
+.. code-block:: html
<a href="/howdy/jane/doe">Howdy</a>
-In ``home.pt`` we switched to::
+In ``home.pt`` we switched to:
+
+.. code-block:: xml
<a href="${request.route_url('hello', first='jane',
last='doe')}">form</a>
-Pyramid has rich facilities to help generate URLs in a flexible,
-non-error-prone fashion.
+Pyramid has rich facilities to help generate URLs in a flexible, non-error
+prone fashion.
-Extra Credit
+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
- ``POST`` used by ``delete``?
+#. The ``edit`` and ``delete`` views are both receive ``POST`` requests. Why
+ does the ``edit`` view configuration not catch 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?
+#. 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
+#. There is also a ``request.route_path`` API. How does this differ from
``request.route_url``?
.. seealso:: :ref:`class_as_view`, `Weird Stuff You Can Do With
diff --git a/docs/quick_tutorial/package.rst b/docs/quick_tutorial/package.rst
index 54a6a0bd9..94cb39fc9 100644
--- a/docs/quick_tutorial/package.rst
+++ b/docs/quick_tutorial/package.rst
@@ -3,50 +3,48 @@
============================================
Most modern Python development is done using Python packages, an approach
-Pyramid puts to good use. In this step we redo "Hello World" as a
-minimum Python package inside a minimum Python project.
+Pyramid puts to good use. In this step we redo "Hello World" as a minimal
+Python package inside a minimal Python project.
+
Background
==========
Python developers can organize a collection of modules and files into a
-namespaced unit called a :ref:`package <python:tut-packages>`. If a
-directory is on ``sys.path`` and has a special file named
-``__init__.py``, it is treated as a Python package.
+namespaced unit called a :ref:`package <python:tut-packages>`. 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 <http://pythonhosted.org/setuptools/setuptools.html>`_.
-Explaining it all in this
-tutorial will induce madness. For this tutorial, this is all you need to
-know:
+Packages can be bundled up, made available for installation, and installed
+through a toolchain oriented around a ``setup.py`` file. For this tutorial,
+this is all you need to know:
-- We will have a directory for each tutorial step as a setuptools *project*
+- We will have a directory for each tutorial step as a *project*.
-- This project will contain a ``setup.py`` which injects the features
- of the setuptool's project machinery into the directory
+- This project will contain a ``setup.py`` which injects the features of the
+ 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
+ *package* using an ``__init__.py`` Python module file.
-- We will run ``python setup.py develop`` to install our project in
- development mode
+- We will run ``pip install -e .`` to install our project in development mode.
In summary:
-- You'll do your development in a Python *package*
+- You'll do your development in a Python *package*.
+
+- That package will be part of a *project*.
-- That package will be part of a setuptools *project*
Objectives
==========
-- Make a Python "package" directory with an ``__init__.py``
+- Make a Python "package" directory with an ``__init__.py``.
-- Get a minimum Python "project" in place by making a ``setup.py``
+- Get a minimum Python "project" in place by making a ``setup.py``.
+
+- Install our ``tutorial`` project in development mode.
-- Install our ``tutorial`` project in development mode
Steps
=====
@@ -61,12 +59,12 @@ Steps
.. literalinclude:: package/setup.py
-#. Make the new project installed for development then make a directory
- for the actual code:
+#. Make the new project installed for development then make a directory for the
+ actual code:
.. code-block:: bash
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
$ mkdir tutorial
#. Enter the following into ``package/tutorial/__init__.py``:
@@ -85,27 +83,29 @@ Steps
#. 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.)
+Python packages give us an organized unit of project development. Python
+projects, via ``setup.py``, give us special features when our package is
+installed (in this case, in local development mode, also called local editable
+mode as indicated by ``-e .``).
-In this step we have a Python package called ``tutorial``. We use the
-same name in each step of the tutorial, to avoid unnecessary retyping.
+In this step we have a Python package called ``tutorial``. We use the same name
+in each step of the tutorial, to avoid unnecessary retyping.
-Above this ``tutorial`` directory we have the files that handle the
-packaging of this project. At the moment, all we need is a
-bare-bones ``setup.py``.
+Above this ``tutorial`` directory we have the files that handle the packaging
+of this project. At the moment, all we need is a bare-bones ``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.
+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
+tries to capture how this stuff works one 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 <python:tut-packages>`,
- `setuptools Entry Points <http://pythonhosted.org/setuptools/pkg_resources.html#entry-points>`_
+.. seealso:: :ref:`Python Packages <python:tut-packages>` and `Working in
+ "Development Mode"
+ <https://packaging.python.org/en/latest/distributing/#working-in-development-mode>`_.
diff --git a/docs/quick_tutorial/request_response.rst b/docs/quick_tutorial/request_response.rst
index 4f8de0221..0ac9b4f6d 100644
--- a/docs/quick_tutorial/request_response.rst
+++ b/docs/quick_tutorial/request_response.rst
@@ -5,33 +5,32 @@
=======================================
Web applications handle incoming requests and return outgoing responses.
-Pyramid makes working with requests and responses convenient and
-reliable.
+Pyramid makes working with requests and responses convenient and reliable.
+
Objectives
==========
-- Learn the background on Pyramid's choices for requests and responses
+- Learn the background on Pyramid's choices for requests and responses.
+
+- Grab data out of the request.
-- Grab data out of the request
+- Change information in the response headers.
-- 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.
+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, and
+so on). 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 <webob_chapter>`.
-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 <webob_chapter>`.
Steps
=====
@@ -41,7 +40,7 @@ Steps
.. code-block:: bash
$ cd ..; cp -r view_classes request_response; cd request_response
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. Simplify the routes in ``request_response/tutorial/__init__.py``:
@@ -62,7 +61,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ .....
+ 5 passed in 0.30 seconds
#. Run your Pyramid application with:
@@ -70,37 +71,39 @@ Steps
$ $VENV/bin/pserve development.ini --reload
-#. Open http://localhost:6543/ in your browser. You will be
- redirected to http://localhost:6543/plain
+#. 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 <http_redirect>` by returning a
-special object from a view or raising a special exception.
+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
+<http_redirect>` 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:
-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::
+.. code-block:: text
URL http://localhost:6543/plain?name=alice with name: alice
-Finally, we set the response's content type and body, then return the
-Response.
+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``.
-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
+Extra credit
============
-#. Could we also ``raise HTTPFound(location='/plain')`` instead of
- returning it? If so, what's the difference?
+#. Could we also ``raise HTTPFound(location='/plain')`` instead of returning
+ it? If so, what's the difference?
.. seealso:: :ref:`webob_chapter`,
:ref:`generate redirects <http_redirect>`
diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst
index f855dcb55..1f2b4da97 100644
--- a/docs/quick_tutorial/requirements.rst
+++ b/docs/quick_tutorial/requirements.rst
@@ -4,75 +4,67 @@
Requirements
============
-Let's get our tutorial environment setup. Most of the setup work is in
-standard Python development practices (install Python,
-make an isolated environment, and setup packaging tools.)
+Let's get our tutorial environment set up. Most of the set up work is in
+standard Python development practices (install Python and make an isolated
+virtual environment.)
.. note::
- Pyramid encourages standard Python development practices with
- packaging tools, virtual environments, logging, and so on. There
- are many variations, implementations, and opinions across the Python
- community. For consistency, ease of documentation maintenance,
- and to minimize confusion, the Pyramid *documentation* has adopted
- specific conventions.
+ Pyramid encourages standard Python development practices with packaging
+ tools, virtual environments, logging, and so on. There are many variations,
+ implementations, and opinions across the Python community. For consistency,
+ ease of documentation maintenance, and to minimize confusion, the Pyramid
+ *documentation* has adopted specific conventions that are consistent with the
+ :term:`Python Packaging Authority`.
This *Quick Tutorial* is based on:
-* **Python 3.3**. Pyramid fully supports Python 3.3+ and Python 2.6+.
- This tutorial uses **Python 3.3** but runs fine under Python 2.7.
+* **Python 3.5**. Pyramid fully supports Python 3.3+ and Python 2.7+. This
+ tutorial uses **Python 3.5** but runs fine under Python 2.7.
-* **pyvenv**. We believe in virtual environments. For this tutorial,
- we use Python 3.3's built-in solution, the ``pyvenv`` command.
- For Python 2.7, you can install ``virtualenv``.
+* **venv**. We believe in virtual environments. For this tutorial, we use
+ Python 3.5's built-in solution :term:`venv`. For Python 2.7, you can install
+ :term:`virtualenv`.
-* **setuptools and easy_install**. We use
- `setuptools <https://pypi.python.org/pypi/setuptools/>`_
- and its ``easy_install`` for package management.
+* **pip**. We use :term:`pip` for package management.
-* **Workspaces, projects, and packages.** Our home directory
- will contain a *tutorial workspace* with our Python virtual
- environment(s) and *Python projects* (a directory with packaging
- information and *Python packages* of working code.)
+* **Workspaces, projects, and packages.** Our home directory will contain a
+ *tutorial workspace* with our Python virtual environment and *Python
+ projects* (a directory with packaging information and *Python packages* of
+ working code.)
-* **Unix commands**. Commands in this tutorial use UNIX syntax and
- paths. Windows users should adjust commands accordingly.
+* **Unix commands**. Commands in this tutorial use UNIX syntax and paths.
+ Windows users should adjust commands accordingly.
.. note::
-
Pyramid was one of the first web frameworks to fully support Python 3 in
October 2011.
+.. note::
+ Windows commands use the plain old MSDOS shell. For PowerShell command
+ syntax, see its documentation.
+
Steps
=====
-#. :ref:`install-python-3.3-or-greater`
+#. :ref:`install-python-3`
#. :ref:`create-a-project-directory-structure`
#. :ref:`set-an-environment-variable`
#. :ref:`create-a-virtual-environment`
-#. :ref:`install-setuptools-(python-packaging-tools)`
#. :ref:`install-pyramid`
-.. _install-python-3.3-or-greater:
-Install Python 3.3 or greater
------------------------------
+.. _install-python-3:
-Download the latest standard Python 3.3+ release (not development release)
-from `python.org <https://www.python.org/downloads/>`_.
+Install Python 3
+----------------
-Windows and Mac OS X users can download and run an installer.
+See the detailed recommendation for your operating system described under
+:ref:`installing_chapter`.
-Windows users should also install the `Python for Windows extensions
-<http://sourceforge.net/projects/pywin32/files/pywin32/>`_. Carefully read the
-``README.txt`` file at the end of the list of builds, and follow its
-directions. Make sure you get the proper 32- or 64-bit build and Python
-version.
-
-Linux users can either use their package manager to install Python 3.3
-or may `build Python 3.3 from source
-<http://pyramid.readthedocs.org/en/master/narr/install.html#package-manager-
-method>`_.
+- :ref:`for-mac-os-x-users`
+- :ref:`if-you-don-t-yet-have-a-python-interpreter-unix`
+- :ref:`if-you-don-t-yet-have-a-python-interpreter-windows`
.. _create-a-project-directory-structure:
@@ -80,11 +72,10 @@ method>`_.
Create a project directory structure
------------------------------------
-We will arrive at a directory structure of
-``workspace->project->package``, with our workspace named
-``quick_tutorial``. The following tree diagram shows how this will be
-structured and where our virtual environment will reside as we proceed through
-the tutorial:
+We will arrive at a directory structure of ``workspace -> project -> package``,
+where our workspace is named ``quick_tutorial``. The following tree diagram
+shows how this will be structured, and where our :term:`virtual environment`
+will reside as we proceed through the tutorial:
.. code-block:: text
@@ -109,105 +100,84 @@ For Linux, the commands to do so are as follows:
For Windows:
-.. code-block:: ps1con
+.. code-block:: doscon
# Windows
c:\> cd \
c:\> mkdir projects\quick_tutorial
c:\> cd projects\quick_tutorial
-In the above figure, your user home directory is represented by ``~``. In
-your home directory, all of your projects are in the ``projects`` directory.
-This is a general convention not specific to Pyramid that many developers use.
-Windows users will do well to use ``c:\`` as the location for ``projects`` in
-order to avoid spaces in any of the path names.
+In the above figure, your user home directory is represented by ``~``. In your
+home directory, all of your projects are in the ``projects`` directory. This is
+a general convention not specific to Pyramid that many developers use. Windows
+users will do well to use ``c:\`` as the location for ``projects`` in order to
+avoid spaces in any of the path names.
Next within ``projects`` is your workspace directory, here named
``quick_tutorial``. A workspace is a common term used by integrated
-development environments (IDE) like PyCharm and PyDev that stores
-isolated Python environments (virtualenvs) and specific project files
-and repositories.
+development environments (IDE), like PyCharm and PyDev, where virtual
+environments, specific project files, and repositories are stored.
.. _set-an-environment-variable:
-Set an Environment Variable
+Set an environment variable
---------------------------
-This tutorial will refer frequently to the location of the virtual
-environment. We set an environment variable to save typing later.
+This tutorial will refer frequently to the location of the :term:`virtual
+environment`. We set an environment variable to save typing later.
.. code-block:: bash
# Mac and Linux
$ export VENV=~/projects/quick_tutorial/env
+.. code-block:: doscon
+
# Windows
- # TODO: This command does not work
c:\> set VENV=c:\projects\quick_tutorial\env
.. _create-a-virtual-environment:
-Create a Virtual Environment
+Create a virtual environment
----------------------------
-.. warning:: The current state of isolated Python environments using
- ``pyvenv`` on Windows is suboptimal in comparison to Mac and Linux. See
- http://stackoverflow.com/q/15981111/95735 for a discussion of the issue
- and `PEP 453 <http://www.python.org/dev/peps/pep-0453/>`_ for a proposed
- resolution.
-
-``pyvenv`` is a tool to create isolated Python 3.3 environments, each
-with its own Python binary and independent set of installed Python
-packages in its site directories. Let's create one, using the location
-we just specified in the environment variable.
+``venv`` is a tool to create isolated Python 3 environments, each with its own
+Python binary and independent set of installed Python packages in its site
+directories. Let's create one, using the location we just specified in the
+environment variable.
.. code-block:: bash
# Mac and Linux
- $ pyvenv $VENV
+ $ python3 -m venv $VENV
- # Windows
- c:\> c:\Python33\python -m venv %VENV%
+.. code-block:: doscon
-.. seealso:: See also Python 3's :mod:`venv module <python3:venv>`,
- Python 2's `virtualenv <http://www.virtualenv.org/en/latest/>`_
- package,
- :ref:`Installing Pyramid on a Windows System <installing_windows>`
+ # Windows
+ c:\> c:\Python35\python3 -m venv %VENV%
+.. seealso:: See also Python 3's :mod:`venv module <python:venv>` and Python
+ 2's `virtualenv <https://virtualenv.pypa.io/en/latest/>`_ package.
-.. _install-setuptools-(python-packaging-tools):
-Install ``setuptools`` (Python packaging tools)
------------------------------------------------
+Update packaging tools in the virtual environment
+-------------------------------------------------
-The following command will download a script to install ``setuptools``, then
-pipe it to your environment's version of Python.
+It's always a good idea to update to the very latest version of packaging tools
+because the installed Python bundles only the version that was available at the
+time of its release.
.. code-block:: bash
# Mac and Linux
- $ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $VENV/bin/python
-
- # Windows
- #
- # Use your web browser to download this file:
- # https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py
- #
- # ...and save it to:
- # c:\projects\quick_tutorial\ez_setup.py
- #
- # Then run the following command:
-
- c:\> %VENV%\Scripts\python ez_setup.py
+ $VENV/bin/pip install --upgrade pip setuptools
-If ``wget`` complains with a certificate error, then run this command instead:
+.. code-block:: doscon
-.. code-block:: bash
-
- # Mac and Linux
- $ wget --no-check-certificate https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $VENV/bin/python
+ # Windows
+ c:\> %VENV%\Scripts\pip install --upgrade pip setuptools
.. _install-pyramid:
@@ -216,48 +186,29 @@ Install Pyramid
---------------
We have our Python standard prerequisites out of the way. The Pyramid
-part is pretty easy:
+part is pretty easy.
.. parsed-literal::
# Mac and Linux
- $ $VENV/bin/easy_install "pyramid==\ |release|\ "
+ $ $VENV/bin/pip install "pyramid==\ |release|\ "
# Windows
- c:\\> %VENV%\\Scripts\\easy_install "pyramid==\ |release|\ "
+ c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ "
Our Python virtual environment now has the Pyramid software available.
-You can optionally install some of the extra Python packages used
-during this tutorial:
+You can optionally install some of the extra Python packages used in this
+tutorial.
.. code-block:: bash
# Mac and Linux
- $ $VENV/bin/easy_install nose webtest deform sqlalchemy \
- pyramid_chameleon pyramid_debugtoolbar waitress \
- pyramid_tm zope.sqlalchemy
-
- # Windows
- c:\> %VENV%\Scripts\easy_install nose webtest deform sqlalchemy pyramid_chameleon pyramid_debugtoolbar waitress pyramid_tm zope.sqlalchemy
+ $ $VENV/bin/pip install webtest pytest pytest-cov deform sqlalchemy \
+ pyramid_chameleon pyramid_debugtoolbar pyramid_jinja2 waitress \
+ pyramid_tm zope.sqlalchemy
+.. code-block:: doscon
-.. note::
-
- Why ``easy_install`` and not ``pip``? Pyramid encourages use of namespace
- packages, for which ``pip``'s support is less-than-optimal. Also, Pyramid's
- dependencies use some optional C extensions for performance: with
- ``easy_install``, Windows users can get these extensions without needing
- a C compiler (``pip`` does not support installing binary Windows
- distributions, except for ``wheels``, which are not yet available for
- all dependencies).
-
-.. seealso:: See also :ref:`installing_unix`. For instructions to set up your
- Python environment for development using Windows or Python 2, see Pyramid's
- :ref:`Before You Install <installing_chapter>`.
-
- See also Python 3's :mod:`venv module <python3:venv>`, the `setuptools
- installation instructions
- <https://pypi.python.org/pypi/setuptools/0.9.8#installation-instructions>`_,
- and `easy_install help <https://pypi.python.org/pypi/setuptools/0.9.8#using-setuptools-and-easyinstall>`_.
-
+ # Windows
+ c:\> %VENV%\Scripts\pip install webtest deform sqlalchemy pyramid_chameleon pyramid_debugtoolbar pyramid_jinja2 waitress pyramid_tm zope.sqlalchemy
diff --git a/docs/quick_tutorial/routing.rst b/docs/quick_tutorial/routing.rst
index 1b79a5889..27c8c2c22 100644
--- a/docs/quick_tutorial/routing.rst
+++ b/docs/quick_tutorial/routing.rst
@@ -4,41 +4,44 @@
11: Dispatching URLs To Views With Routing
==========================================
-Routing matches incoming URL patterns to view code. Pyramid's routing
-has a number of useful features.
+Routing matches incoming URL patterns to view code. Pyramid's routing has a
+number of useful features.
+
Background
==========
-Writing web applications usually means sophisticated URL design. We
-just saw some Pyramid machinery for requests and views. Let's look at
-features that help in routing.
+Writing web applications usually means sophisticated URL design. We just saw
+some Pyramid machinery for requests and views. Let's look at features that help
+in routing.
Previously we saw the basics of routing URLs to views in Pyramid.
-- Your project's "setup" code registers a route name to be used when
- matching part of the URL
+- Your project's "setup" code registers a route name to be used when matching
+ part of the URL
-- Elsewhere, a view is configured to be called for that route name
+- Elsewhere a view is configured to be called for that route name.
.. note::
- Why do this twice? Other Python web frameworks let you create a
- route and associate it with a view in one step. As
- illustrated in :ref:`routes_need_ordering`, multiple routes might match the
- same URL pattern. Rather than provide ways to help guess, Pyramid lets you
- be explicit in ordering. Pyramid also gives facilities to avoid the
- problem. It's relatively easy to build a system that uses implicit route
- ordering with Pyramid too. See `The Groundhog series of screencasts
- <http://bfg.repoze.org/videos#groundhog1>`_ if you're interested in
+ Why do this twice? Other Python web frameworks let you create a route and
+ associate it with a view in one step. As illustrated in
+ :ref:`routes_need_ordering`, multiple routes might match the same URL
+ pattern. Rather than provide ways to help guess, Pyramid lets you be
+ explicit in ordering. Pyramid also gives facilities to avoid the problem.
+ It's relatively easy to build a system that uses implicit route ordering
+ with Pyramid too. See `The Groundhog series of screencasts
+ <http://static.repoze.org/casts/videotags.html>`_ if you're interested in
doing so.
+
Objectives
==========
-- Define a route that extracts part of the URL into a Python dictionary
+- Define a route that extracts part of the URL into a Python dictionary.
+
+- Use that dictionary data in a view.
-- Use that dictionary data in a view
Steps
=====
@@ -48,7 +51,7 @@ Steps
.. code-block:: bash
$ cd ..; cp -r view_classes routing; cd routing
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. Our ``routing/tutorial/__init__.py`` needs a route with a replacement
pattern:
@@ -76,7 +79,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
+ $ $VENV/bin/$VENV/bin/py.test tutorial/tests.py -q
+ ..
+ 2 passed in 0.39 seconds
#. Run your Pyramid application with:
@@ -86,6 +91,7 @@ Steps
#. Open http://localhost:6543/howdy/amy/smith in your browser.
+
Analysis
========
@@ -95,27 +101,24 @@ In ``__init__.py`` we see an important change in our route declaration:
config.add_route('hello', '/howdy/{first}/{last}')
-With this we tell the :term:`configurator` that our URL has
-a "replacement pattern". With this, URLs such as ``/howdy/amy/smith``
-will assign ``amy`` to ``first`` and ``smith`` to ``last``. We can then
-use this data in our view:
+With this we tell the :term:`configurator` that our URL has a "replacement
+pattern". With this, URLs such as ``/howdy/amy/smith`` will assign ``amy`` to
+``first`` and ``smith`` to ``last``. We can then use this data in our view:
.. code-block:: python
self.request.matchdict['first']
self.request.matchdict['last']
-``request.matchdict`` contains values from the URL that match the
-"replacement patterns" (the curly braces) in the route declaration.
-This information can then be used anywhere in Pyramid that has access
-to the request.
+``request.matchdict`` contains values from the URL that match the "replacement
+patterns" (the curly braces) in the route declaration. This information can
+then be used anywhere in Pyramid that has access to the request.
-Extra Credit
+Extra credit
============
-#. What happens if you to go the URL
- http://localhost:6543/howdy? Is this the result that you
- expected?
+#. What happens if you to go the URL http://localhost:6543/howdy? Is this the
+ result that you expected?
-.. seealso:: `Weird Stuff You Can Do With URL
- Dispatch <http://www.plope.com/weird_pyramid_urldispatch>`_
+.. seealso:: `Weird Stuff You Can Do With URL Dispatch
+ <http://www.plope.com/weird_pyramid_urldispatch>`_
diff --git a/docs/quick_tutorial/scaffolds.rst b/docs/quick_tutorial/scaffolds.rst
index 4f2694100..7845f2b71 100644
--- a/docs/quick_tutorial/scaffolds.rst
+++ b/docs/quick_tutorial/scaffolds.rst
@@ -4,29 +4,30 @@
Prelude: Quick Project Startup with Scaffolds
=============================================
-To ease the process of getting started, Pyramid provides *scaffolds*
-that generate sample projects from templates in Pyramid and Pyramid
-add-ons.
+To ease the process of getting started, Pyramid provides *scaffolds* that
+generate sample projects from templates in Pyramid and Pyramid add-ons.
+
Background
==========
-We're going to cover a lot in this tutorial, focusing on one topic at a
-time and writing everything from scratch. As a warmup, though,
-it sure would be nice to see some pixels on a screen.
+We're going to cover a lot in this tutorial, focusing on one topic at a time
+and writing everything from scratch. As a warm up, though, it sure would be
+nice to see some pixels on a screen.
+
+Like other web development frameworks, Pyramid provides a number of "scaffolds"
+that generate working Python, template, and CSS code for sample applications.
+In this step we'll use a built-in scaffold to let us preview a Pyramid
+application, before starting from scratch on Step 1.
-Like other web development frameworks, Pyramid provides a number of
-"scaffolds" that generate working Python, template, and CSS code for
-sample applications. In this step we'll use a built-in scaffold to let
-us preview a Pyramid application, before starting from scratch on Step 1.
Objectives
==========
-- Use Pyramid's ``pcreate`` command to list scaffolds and make a new
- project
+- Use Pyramid's ``pcreate`` command to list scaffolds and make a new project.
+
+- Start up a Pyramid application and visit it in a web browser.
-- Start up a Pyramid application and visit it in a web browser
Steps
=====
@@ -47,21 +48,22 @@ Steps
$ $VENV/bin/pcreate --scaffold starter scaffolds
-#. Use normal Python development to setup our project for development:
+#. Install our project in editable mode for development in the current
+ directory:
.. code-block:: bash
$ cd scaffolds
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
-#. Startup the application by pointing Pyramid's ``pserve`` command at
- the project's (generated) configuration file:
+#. Start up the application by pointing Pyramid's ``pserve`` command at the
+ project's (generated) configuration file:
.. code-block:: bash
$ $VENV/bin/pserve development.ini --reload
- On startup, ``pserve`` logs some output:
+ On start up, ``pserve`` logs some output:
.. code-block:: bash
@@ -74,13 +76,12 @@ Steps
Analysis
========
-Rather than starting from scratch, ``pcreate`` can make getting a
-Python project containing a Pyramid application a quick matter.
-Pyramid ships with a few scaffolds. But installing a Pyramid add-on can
-give you new scaffolds from that add-on.
+Rather than starting from scratch, ``pcreate`` can make getting a Python
+project containing a Pyramid application a quick matter. Pyramid ships with a
+few scaffolds. But installing a Pyramid add-on can give you new scaffolds from
+that add-on.
-``pserve`` is Pyramid's application runner, separating operational
-details from your code. When you install Pyramid, a small command
-program called ``pserve`` is written to your ``bin`` directory. This
-program is an executable Python module. It is passed a configuration
-file (in this case, ``development.ini``.)
+``pserve`` is Pyramid's application runner, separating operational details from
+your code. When you install Pyramid, a small command program called ``pserve``
+is written to your ``bin`` directory. This program is an executable Python
+module. It is passed a configuration file (in this case, ``development.ini``).
diff --git a/docs/quick_tutorial/sessions.rst b/docs/quick_tutorial/sessions.rst
index f97405500..df4887a4b 100644
--- a/docs/quick_tutorial/sessions.rst
+++ b/docs/quick_tutorial/sessions.rst
@@ -6,25 +6,28 @@
Store and retrieve non-permanent data in Pyramid sessions.
+
Background
==========
-When people use your web application, they frequently perform a task
-that requires semi-permanent data to be saved. For example, a shopping
-cart. This is called a :term:`session`.
+When people use your web application, they frequently perform a task that
+requires semi-permanent data to be saved. For example, a shopping cart. This is
+called a :term:`session`.
Pyramid has basic built-in support for sessions. Third party packages such as
-``pyramid_redis_sessions`` provide richer session support. Or you can create
-your own custom sessioning engine. Let's take a look at the
-:doc:`built-in sessioning support <../narr/sessions>`.
+`pyramid_redis_sessions
+<https://github.com/ericrasmussen/pyramid_redis_sessions>`_ provide richer
+session support. Or you can create your own custom sessioning engine. Let's
+take a look at the :doc:`built-in sessioning support <../narr/sessions>`.
+
Objectives
==========
-- Make a session factory using a built-in, simple Pyramid sessioning
- system
+- Make a session factory using a built-in, simple Pyramid sessioning system.
+
+- Change our code to use a session.
-- Change our code to use a session
Steps
=====
@@ -34,16 +37,15 @@ Steps
.. code-block:: bash
$ cd ..; cp -r view_classes sessions; cd sessions
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
-#. Our ``sessions/tutorial/__init__.py`` needs a choice of session
- factory to get registered with the :term:`configurator`:
+#. Our ``sessions/tutorial/__init__.py`` needs a choice of session factory to
+ get registered with the :term:`configurator`:
.. literalinclude:: sessions/tutorial/__init__.py
:linenos:
-#. Our views in ``sessions/tutorial/views.py`` can now use
- ``request.session``:
+#. Our views in ``sessions/tutorial/views.py`` can now use ``request.session``:
.. literalinclude:: sessions/tutorial/views.py
:linenos:
@@ -58,7 +60,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ....
+ 4 passed in 0.42 seconds
#. Run your Pyramid application with:
@@ -66,33 +70,33 @@ Steps
$ $VENV/bin/pserve development.ini --reload
-#. Open http://localhost:6543/ and http://localhost:6543/howdy
- in your browser. As you reload and switch between those URLs, note
- that the counter increases and is *not* specific to the URL.
+#. Open http://localhost:6543/ and http://localhost:6543/howdy in your browser.
+ As you reload and switch between those URLs, note that the counter increases
+ and is *not* specific to the URL.
+
+#. Restart the application and revisit the page. Note that counter still
+ increases from where it left off.
-#. Restart the application and revisit the page. Note that counter
- still increases from where it left off.
Analysis
========
-Pyramid's :term:`request` object now has a ``session`` attribute
-that we can use in our view code. It acts like a dictionary.
+Pyramid's :term:`request` object now has a ``session`` attribute that we can
+use in our view code. It acts like a dictionary.
-Since all the views are using the same counter, we made the counter a
-Python property at the view class level. With this, each reload will
-increase the counter displayed in our template.
+Since all the views are using the same counter, we made the counter a Python
+property at the view class level. With this, each reload will increase the
+counter displayed in our template.
-In web development, "flash messages" are notes for the user that need
-to appear on a screen after a future web request. For example,
-when you add an item using a form ``POST``, the site usually issues a
-second HTTP Redirect web request to view the new item. You might want a
-message to appear after that second web request saying "Your item was
-added." You can't just return it in the web response for the POST,
-as it will be tossed out during the second web request.
+In web development, "flash messages" are notes for the user that need to appear
+on a screen after a future web request. For example, when you add an item using
+a form ``POST``, the site usually issues a second HTTP Redirect web request to
+view the new item. You might want a message to appear after that second web
+request saying "Your item was added." You can't just return it in the web
+response for the POST, as it will be tossed out during the second web request.
-Flash messages are a technique where messages can be stored between
-requests, using sessions, then removed when they finally get displayed.
+Flash messages are a technique where messages can be stored between requests,
+using sessions, then removed when they finally get displayed.
.. seealso::
:ref:`sessions_chapter`,
diff --git a/docs/quick_tutorial/static_assets.rst b/docs/quick_tutorial/static_assets.rst
index 3a7496ec7..65b34f8f9 100644
--- a/docs/quick_tutorial/static_assets.rst
+++ b/docs/quick_tutorial/static_assets.rst
@@ -4,16 +4,17 @@
13: CSS/JS/Images Files With Static Assets
==========================================
-Of course the Web is more than just markup. You need static assets:
-CSS, JS, and images. Let's point our web app at a directory where
-Pyramid will serve some static assets.
+Of course the Web is more than just markup. You need static assets: CSS, JS,
+and images. Let's point our web app at a directory where Pyramid will serve
+some static assets.
Objectives
==========
-- Publish a directory of static assets at a URL
+- Publish a directory of static assets at a URL.
+
+- Use Pyramid to help generate URLs to files in that directory.
-- Use Pyramid to help generate URLs to files in that directory
Steps
=====
@@ -23,7 +24,7 @@ Steps
.. code-block:: bash
$ cd ..; cp -r view_classes static_assets; cd static_assets
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. We add a call ``config.add_static_view`` in
``static_assets/tutorial/__init__.py``:
@@ -37,8 +38,7 @@ Steps
.. literalinclude:: static_assets/tutorial/home.pt
:language: html
-#. Add a CSS file at
- ``static_assets/tutorial/static/app.css``:
+#. Add a CSS file at ``static_assets/tutorial/static/app.css``:
.. literalinclude:: static_assets/tutorial/static/app.css
:language: css
@@ -47,7 +47,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
+ $ $VENV/bin/$VENV/bin/py.test tutorial/tests.py -q
+ ....
+ 4 passed in 0.50 seconds
#. Run your Pyramid application with:
@@ -57,30 +59,31 @@ Steps
#. Open http://localhost:6543/ in your browser and note the new font.
+
Analysis
========
We changed our WSGI application to map requests under
-http://localhost:6543/static/ to files and directories inside a
-``static`` directory inside our ``tutorial`` package. This directory
-contained ``app.css``.
+http://localhost:6543/static/ to files and directories inside a ``static``
+directory inside our ``tutorial`` package. This directory contained
+``app.css``.
-We linked to the CSS in our template. We could have hard-coded this
-link to ``/static/app.css``. But what if the site is later moved under
-``/somesite/static/``? Or perhaps the web developer changes the
-arrangement on disk? Pyramid gives a helper that provides flexibility
-on URL generation:
+We linked to the CSS in our template. We could have hard-coded this link to
+``/static/app.css``. But what if the site is later moved under
+``/somesite/static/``? Or perhaps the web developer changes the arrangement on
+disk? Pyramid gives a helper that provides flexibility on URL generation:
.. code-block:: html
${request.static_url('tutorial:static/app.css')}
-This matches the ``path='tutorial:static'`` in our
-``config.add_static_view`` registration. By using ``request.static_url``
-to generate the full URL to the static assets, you both ensure you stay
-in sync with the configuration and gain refactoring flexibility later.
+This matches the ``path='tutorial:static'`` in our ``config.add_static_view``
+registration. By using ``request.static_url`` to generate the full URL to the
+static assets, you both ensure you stay in sync with the configuration and gain
+refactoring flexibility later.
+
-Extra Credit
+Extra credit
============
#. There is also a ``request.static_path`` API. How does this differ from
diff --git a/docs/quick_tutorial/templating.rst b/docs/quick_tutorial/templating.rst
index cf56d2a96..ec6de98f8 100644
--- a/docs/quick_tutorial/templating.rst
+++ b/docs/quick_tutorial/templating.rst
@@ -4,50 +4,53 @@
08: HTML Generation With Templating
===================================
-Most web frameworks don't embed HTML in programming code. Instead,
-they pass data into a templating system. In this step we look at the
-basics of using HTML templates in Pyramid.
+Most web frameworks don't embed HTML in programming code. Instead, they pass
+data into a templating system. In this step we look at the basics of using HTML
+templates in Pyramid.
+
Background
==========
-Ouch. We have been making our own ``Response`` and filling the response
-body with HTML. You usually won't embed an HTML string directly in
-Python, but instead, will use a templating language.
+Ouch. We have been making our own ``Response`` and filling the response body
+with HTML. You usually won't embed an HTML string directly in Python, but
+instead will use a templating language.
+
+Pyramid doesn't mandate a particular database system, form library, and so on.
+It encourages replaceability. This applies equally to templating, which is
+fortunate: developers have strong views about template languages. As of
+Pyramid 1.5a2, Pyramid doesn't even bundle a template language!
-Pyramid doesn't mandate a particular database system, form library,
-etc. It encourages replaceability. This applies equally to templating,
-which is fortunate: developers have strong views about template
-languages. As of Pyramid 1.5a2, Pyramid doesn't even bundle a template
-language!
+It does, however, have strong ties to Jinja2, Mako, and Chameleon. In this step
+we see how to add `pyramid_chameleon
+<https://github.com/Pylons/pyramid_chameleon>`_ to your project, then change
+your views to use templating.
-It does, however, have strong ties to Jinja2, Mako, and Chameleon. In
-this step we see how to add ``pyramid_chameleon`` to your project,
-then change your views to use templating.
Objectives
==========
-- Enable the ``pyramid_chameleon`` Pyramid add-on
+- Enable the ``pyramid_chameleon`` Pyramid add-on.
+
+- Generate HTML from template files.
-- Generate HTML from template files
+- Connect the templates as "renderers" for view code.
-- Connect the templates as "renderers" for view code
+- Change the view code to simply return data.
-- Change the view code to simply return data
Steps
=====
-#. Let's begin by using the previous package as a starting point for a
- new project:
+#. Let's begin by using the previous package as a starting point for a new
+ project:
.. code-block:: bash
$ cd ..; cp -r views templating; cd templating
-#. This step depends on ``pyramid_chameleon``, so add it as a dependency
- in ``templating/setup.py``:
+#. This step depends on ``pyramid_chameleon``, so add it as a dependency in
+ ``templating/setup.py``:
.. literalinclude:: templating/setup.py
:linenos:
@@ -56,10 +59,10 @@ Steps
.. code-block:: bash
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
-#. We need to connect ``pyramid_chameleon`` as a renderer by making a
- call in the setup of ``templating/tutorial/__init__.py``:
+#. We need to connect ``pyramid_chameleon`` as a renderer by making a call in
+ the setup of ``templating/tutorial/__init__.py``:
.. literalinclude:: templating/tutorial/__init__.py
:linenos:
@@ -74,14 +77,13 @@ Steps
.. literalinclude:: templating/tutorial/home.pt
:language: html
-#. For convenience, change ``templating/development.ini`` to reload
- templates automatically with ``pyramid.reload_templates``:
+#. For convenience, change ``templating/development.ini`` to reload templates
+ automatically with ``pyramid.reload_templates``:
.. literalinclude:: templating/development.ini
:language: ini
-#. Our unit tests in ``templating/tutorial/tests.py`` can focus on
- data:
+#. Our unit tests in ``templating/tutorial/tests.py`` can focus on data:
.. literalinclude:: templating/tutorial/tests.py
:linenos:
@@ -90,13 +92,9 @@ Steps
.. code-block:: bash
-
- $ $VENV/bin/nosetests tutorial
- .
- ----------------------------------------------------------------------
- Ran 4 tests in 0.141s
-
- OK
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ....
+ 4 passed in 0.46 seconds
#. Run your Pyramid application with:
@@ -104,20 +102,19 @@ Steps
$ $VENV/bin/pserve development.ini --reload
-#. Open http://localhost:6543/ and http://localhost:6543/howdy
- in your browser.
+#. Open http://localhost:6543/ and http://localhost:6543/howdy in your browser.
+
Analysis
========
-Ahh, that looks better. We have a view that is focused on Python code.
-Our ``@view_config`` decorator specifies a :term:`renderer` that points
-to our template file. Our view then simply returns data which is then
-supplied to our template. Note that we used the same template for both
-views.
+Ahh, that looks better. We have a view that is focused on Python code. Our
+``@view_config`` decorator specifies a :term:`renderer` that points to our
+template file. Our view then simply returns data which is then supplied to our
+template. Note that we used the same template for both views.
-Note the effect on testing. We can focus on having a data-oriented
-contract with our view code.
+Note the effect on testing. We can focus on having a data-oriented contract
+with our view code.
.. seealso:: :ref:`templates_chapter`, :ref:`debugging_templates`, and
:ref:`available_template_system_bindings`.
diff --git a/docs/quick_tutorial/tutorial_approach.rst b/docs/quick_tutorial/tutorial_approach.rst
index 204d388b0..6d534fe13 100644
--- a/docs/quick_tutorial/tutorial_approach.rst
+++ b/docs/quick_tutorial/tutorial_approach.rst
@@ -2,44 +2,46 @@
Tutorial Approach
=================
-This tutorial uses conventions to keep the introduction focused and
-concise. Details, references, and deeper discussions are mentioned in
-"See also" notes.
+This tutorial uses conventions to keep the introduction focused and concise.
+Details, references, and deeper discussions are mentioned in "See also" notes.
.. seealso:: This is an example "See also" note.
-This "Getting Started" tutorial is broken into independent steps,
-starting with the smallest possible "single file WSGI app" example.
-Each of these steps introduce a topic and a very small set of concepts
-via working code. The steps each correspond to a directory in this
-repo, where each step/topic/directory is a Python package.
+This "Getting Started" tutorial is broken into independent steps, starting with
+the smallest possible "single file WSGI app" example. Each of these steps
+introduce a topic and a very small set of concepts via working code. The steps
+each correspond to a directory in this repo, where each step/topic/directory is
+a Python package.
-To successfully run each step::
+To successfully run each step:
- $ cd request_response
- $ $VENV/bin/python setup.py develop
+.. code-block:: bash
-...and repeat for each step you would like to work on. In most cases we
-will start with the results of an earlier step.
+ $ cd request_response
+ $ $VENV/bin/pip install -e .
-Directory Tree
+...and repeat for each step you would like to work on. In most cases we will
+start with the results of an earlier step.
+
+Directory tree
==============
-As we develop our tutorial our directory tree will resemble the
-structure below::
-
- quicktutorial/
- request_response/
- development.ini
- setup.py
- tutorial/
- __init__.py
- home.pt
- tests.py
- views.py
-
-Each of the first-level directories (e.g. ``request_response``) is a
-*Python project* (except, as noted, the ``hello_world`` step.) The
-``tutorial`` directory is a *Python package*. At the end of each step,
-we copy a previous directory into a new directory to use as a starting
-point.
+As we develop our tutorial, our directory tree will resemble the structure
+below:
+
+.. code-block:: text
+
+ quick_tutorial
+ ├── env
+ └── request_response
+ ├── tutorial
+ │ ├── __init__.py
+ │ ├── tests.py
+ │ └── views.py
+ ├── development.ini
+ └── setup.py
+
+Each of the first-level directories (e.g., ``request_response``) is a *Python
+project* (except as noted for the ``hello_world`` step). The ``tutorial``
+directory is a *Python package*. At the end of each step, we copy a previous
+directory into a new directory to use as a starting point.
diff --git a/docs/quick_tutorial/unit_testing.rst b/docs/quick_tutorial/unit_testing.rst
index 4cb7ef714..56fd2b297 100644
--- a/docs/quick_tutorial/unit_testing.rst
+++ b/docs/quick_tutorial/unit_testing.rst
@@ -1,55 +1,56 @@
.. _qtut_unit_testing:
-===========================
-05: Unit Tests and ``nose``
-===========================
+=============================
+05: Unit Tests and ``pytest``
+=============================
Provide unit testing for our project's Python code.
+
Background
==========
-As the mantra says, "Untested code is broken code." The Python
-community has had a long culture of writing test scripts which ensure
-that your code works correctly as you write it and maintain it in the
-future. Pyramid has always had a deep commitment to testing,
-with 100% test coverage from the earliest pre-releases.
-
-Python includes a
-:ref:`unit testing framework <python:unittest-minimal-example>` in its
-standard library. Over the years a number of Python projects, such as
-`nose <https://pypi.python.org/pypi/nose/>`_, have extended this
-framework with alternative test runners that provide more convenience
-and functionality. The Pyramid developers use ``nose``, which we'll thus
-use in this tutorial.
-
-Don't worry, this tutorial won't be pedantic about "test-driven
-development" (TDD). We'll do just enough to ensure that, in each step,
-we haven't majorly broken the code. As you're writing your code you
-might find this more convenient than changing to your browser
-constantly and clicking reload.
-
-We'll also leave discussion of
-`coverage <https://pypi.python.org/pypi/coverage>`_ for another section.
+As the mantra says, "Untested code is broken code." The Python community has
+had a long culture of writing test scripts which ensure that your code works
+correctly as you write it and maintain it in the future. Pyramid has always had
+a deep commitment to testing, with 100% test coverage from the earliest
+pre-releases.
+
+Python includes a :ref:`unit testing framework
+<python:unittest-minimal-example>` in its standard library. Over the years a
+number of Python projects, such as :ref:`pytest <pytest:features>`, have
+extended this framework with alternative test runners that provide more
+convenience and functionality. The Pyramid developers use ``pytest``, which
+we'll use in this tutorial.
+
+Don't worry, this tutorial won't be pedantic about "test-driven development"
+(TDD). We'll do just enough to ensure that, in each step, we haven't majorly
+broken the code. As you're writing your code, you might find this more
+convenient than changing to your browser constantly and clicking reload.
+
+We'll also leave discussion of `pytest-cov
+<http://pytest-cov.readthedocs.org/en/latest/>`_ for another section.
+
Objectives
==========
-- Write unit tests that ensure the quality of our code
+- Write unit tests that ensure the quality of our code.
+
+- Install a Python package (``pytest``) which helps in our testing.
-- Install a Python package (``nose``) which helps in our testing
Steps
=====
-#. First we copy the results of the previous step, as well as install
- the ``nose`` package:
+#. First we copy the results of the previous step, as well as install the
+ ``pytest`` package:
.. code-block:: bash
$ cd ..; cp -r debugtoolbar unit_testing; cd unit_testing
- $ $VENV/bin/python setup.py develop
- $ $VENV/bin/easy_install nose
+ $ $VENV/bin/pip install -e .
+ $ $VENV/bin/pip install pytest
#. Now we write a simple unit test in ``unit_testing/tutorial/tests.py``:
@@ -61,54 +62,51 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
+ $ $VENV/bin/py.test tutorial/tests.py -q
.
- ----------------------------------------------------------------------
- Ran 1 test in 0.141s
+ 1 passed in 0.14 seconds
- OK
Analysis
========
-Our ``tests.py`` imports the Python standard unit testing framework. To
-make writing Pyramid-oriented tests more convenient, Pyramid supplies
-some ``pyramid.testing`` helpers which we use in the test setup and
-teardown. Our one test imports the view, makes a dummy request, and sees
-if the view returns what we expected.
+Our ``tests.py`` imports the Python standard unit testing framework. To make
+writing Pyramid-oriented tests more convenient, Pyramid supplies some
+``pyramid.testing`` helpers which we use in the test setup and teardown. Our
+one test imports the view, makes a dummy request, and sees if the view returns
+what we expect.
-The ``tests.TutorialViewTests.test_hello_world`` test is a small
-example of a unit test. First, we import the view inside each test. Why
-not import at the top, like in normal Python code? Because imports can
-cause effects that break a test. We'd like our tests to be in *units*,
-hence the name *unit* testing. Each test should isolate itself to the
-correct degree.
+The ``tests.TutorialViewTests.test_hello_world`` test is a small example of a
+unit test. First, we import the view inside each test. Why not import at the
+top, like in normal Python code? Because imports can cause effects that break a
+test. We'd like our tests to be in *units*, hence the name *unit* testing. Each
+test should isolate itself to the correct degree.
-Our test then makes a fake incoming web request, then calls our Pyramid
-view. We test the HTTP status code on the response to make sure it
-matches our expectations.
+Our test then makes a fake incoming web request, then calls our Pyramid view.
+We test the HTTP status code on the response to make sure it matches our
+expectations.
Note that our use of ``pyramid.testing.setUp()`` and
``pyramid.testing.tearDown()`` aren't actually necessary here; they are only
necessary when your test needs to make use of the ``config`` object (it's a
Configurator) to add stuff to the configuration state before calling the view.
+
Extra Credit
============
-#. Change the test to assert that the response status code should be
- ``404`` (meaning, not found.) Run ``nosetests`` again. Read the
- error report and see if you can decipher what it is telling you.
+#. Change the test to assert that the response status code should be ``404``
+ (meaning, not found). Run ``py.test`` again. Read the error report and see
+ if you can decipher what it is telling you.
-#. As a more realistic example, put the ``tests.py`` back as you found
- it and put an error in your view, such as a reference to a
- non-existing variable. Run the tests and see how this is more
- convenient than reloading your browser and going back to your code.
+#. As a more realistic example, put the ``tests.py`` back as you found it, and
+ put an error in your view, such as a reference to a non-existing variable.
+ Run the tests and see how this is more convenient than reloading your
+ browser and going back to your code.
#. Finally, for the most realistic test, read about Pyramid ``Response``
- objects and see how to change the response code. Run the tests and
- see how testing confirms the "contract" that your code claims to
- support.
+ objects and see how to change the response code. Run the tests and see how
+ testing confirms the "contract" that your code claims to support.
#. How could we add a unit test assertion to test the HTML value of the
response body?
diff --git a/docs/quick_tutorial/view_classes.rst b/docs/quick_tutorial/view_classes.rst
index 50a7ee0af..05d97a9b1 100644
--- a/docs/quick_tutorial/view_classes.rst
+++ b/docs/quick_tutorial/view_classes.rst
@@ -4,55 +4,55 @@
09: Organizing Views With View Classes
======================================
-Change our view functions to be methods on a view class,
-then move some declarations to the class level.
+Change our view functions to be methods on a view class, then move some
+declarations to the class level.
+
Background
==========
-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 <class_as_view>` makes sense:
+So far our views have been simple, free-standing functions. Many times your
+views are related to one another. They may be different ways to look at or work
+on the same data, or be a REST API that handles multiple operations. Grouping
+these views together as a :ref:`view class <class_as_view>` makes sense:
+
+- Group views.
-- Group views
+- Centralize some repetitive defaults.
-- Centralize some repetitive defaults
+- Share some state and helpers.
-- Share some state and helpers
+In this step we just do the absolute minimum to convert the existing views to a
+view class. In a later tutorial step, we'll examine view classes in depth.
-In this step we just do the absolute minimum to convert the existing
-views to a view class. In a later tutorial step we'll examine view
-classes in depth.
Objectives
==========
-- Group related views into a view class
+- Group related views into a view class.
+
+- Centralize configuration with class-level ``@view_defaults``.
-- Centralize configuration with class-level ``@view_defaults``
Steps
=====
-
#. First we copy the results of the previous step:
.. code-block:: bash
$ cd ..; cp -r templating view_classes; cd view_classes
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
-#. Our ``view_classes/tutorial/views.py`` now has a view class with
- our two views:
+#. Our ``view_classes/tutorial/views.py`` now has a view class with our two
+ views:
.. literalinclude:: view_classes/tutorial/views.py
:linenos:
-#. Our unit tests in ``view_classes/tutorial/tests.py`` don't run,
- so let's modify them to import the view class and make an instance
- before getting a response:
+#. Our unit tests in ``view_classes/tutorial/tests.py`` don't run, so let's
+ modify them to import the view class, and make an instance before getting a
+ response:
.. literalinclude:: view_classes/tutorial/tests.py
:linenos:
@@ -62,12 +62,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
- .
- ----------------------------------------------------------------------
- Ran 4 tests in 0.141s
-
- OK
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ....
+ 4 passed in 0.34 seconds
#. Run your Pyramid application with:
@@ -75,24 +72,23 @@ Steps
$ $VENV/bin/pserve development.ini --reload
-#. Open http://localhost:6543/ and http://localhost:6543/howdy
- in your browser.
+#. Open http://localhost:6543/ and http://localhost:6543/howdy in your browser.
+
Analysis
========
To ease the transition to view classes, we didn't introduce any new
-functionality. We simply changed the view functions to methods on a
-view class, then updated the tests.
-
-In our ``TutorialViews`` view class you can see that our two view
-classes are logically grouped together as methods on a common class.
-Since the two views shared the same template, we could move that to a
-``@view_defaults`` decorator at the class level.
-
-The tests needed to change. Obviously we needed to import the view
-class. But you can also see the pattern in the tests of instantiating
-the view class with the dummy request first, then calling the view
-method being tested.
+functionality. We simply changed the view functions to methods on a view class,
+then updated the tests.
+
+In our ``TutorialViews`` view class, you can see that our two view classes are
+logically grouped together as methods on a common class. Since the two views
+shared the same template, we could move that to a ``@view_defaults`` decorator
+at the class level.
+
+The tests needed to change. Obviously we needed to import the view class. But
+you can also see the pattern in the tests of instantiating the view class with
+the dummy request first, then calling the view method being tested.
.. seealso:: :ref:`class_as_view`
diff --git a/docs/quick_tutorial/views.rst b/docs/quick_tutorial/views.rst
index 6728925fd..edbe4b2ff 100644
--- a/docs/quick_tutorial/views.rst
+++ b/docs/quick_tutorial/views.rst
@@ -6,12 +6,12 @@
Organize a views module with decorators and multiple views.
+
Background
==========
-For the examples so far, the ``hello_world`` function is a "view". In
-Pyramid, views are the primary way to accept web requests and return
-responses.
+For the examples so far, the ``hello_world`` function is a "view". In Pyramid,
+views are the primary way to accept web requests and return responses.
So far our examples place everything in one file:
@@ -23,27 +23,29 @@ So far our examples place everything in one file:
- The WSGI application launcher
-Let's move the views out to their own ``views.py`` module and change
-our startup code to scan that module, looking for decorators that setup
-the views. Let's also add a second view and update our tests.
+Let's move the views out to their own ``views.py`` module and change our
+startup code to scan that module, looking for decorators that set up the views.
+Let's also add a second view and update our tests.
+
Objectives
==========
-- Views in a module that is scanned by the configurator
+- Move views into a module that is scanned by the configurator.
+
+- Create decorators that do declarative configuration.
-- Decorators that do declarative configuration
Steps
=====
-#. Let's begin by using the previous package as a starting point for a
- new distribution, then making it active:
+#. Let's begin by using the previous package as a starting point for a new
+ distribution, then making it active:
.. code-block:: bash
$ cd ..; cp -r functional_testing views; cd views
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
#. Our ``views/tutorial/__init__.py`` gets a lot shorter:
@@ -66,12 +68,9 @@ Steps
.. code-block:: bash
- $ $VENV/bin/nosetests tutorial
- .
- ----------------------------------------------------------------------
- Ran 4 tests in 0.141s
-
- OK
+ $ $VENV/bin/py.test tutorial/tests.py -q
+ ....
+ 4 passed in 0.28 seconds
#. Run your Pyramid application with:
@@ -82,41 +81,41 @@ Steps
#. Open http://localhost:6543/ and http://localhost:6543/howdy
in your browser.
+
Analysis
========
-We added some more URLs, but we also removed the view code from the
-application startup code in ``tutorial/__init__.py``.
-Our views, and their view registrations (via decorators) are now in a
-module ``views.py`` which is scanned via ``config.scan('.views')``.
-
-We have 2 views, each leading to the other. If you start at
-http://localhost:6543/, you get a response with a link to the next
-view. The ``hello`` view (available at the URL ``/howdy``) has a link
-back to the first view.
-
-This step also shows that the name appearing in the URL,
-the name of the "route" that maps a URL to a view,
-and the name of the view, can all be different. More on routes later.
-
-Earlier we saw ``config.add_view`` as one way to configure a view. This
-section introduces ``@view_config``. Pyramid's configuration supports
-:term:`imperative configuration`, such as the
-``config.add_view`` in the previous example. You can also use
-:term:`declarative configuration`, in which a Python
-:term:`python:decorator`
-is placed on the line above the view. Both approaches result in the
-same final configuration, thus usually, it is simply a matter of taste.
-
-Extra Credit
+We added some more URLs, but we also removed the view code from the application
+startup code in ``tutorial/__init__.py``. Our views, and their view
+registrations (via decorators) are now in a module ``views.py``, which is
+scanned via ``config.scan('.views')``.
+
+We have two views, each leading to the other. If you start at
+http://localhost:6543/, you get a response with a link to the next view. The
+``hello`` view (available at the URL ``/howdy``) has a link back to the first
+view.
+
+This step also shows that the name appearing in the URL, the name of the
+"route" that maps a URL to a view, and the name of the view, can all be
+different. More on routes later.
+
+Earlier we saw ``config.add_view`` as one way to configure a view. This section
+introduces ``@view_config``. Pyramid's configuration supports :term:`imperative
+configuration`, such as the ``config.add_view`` in the previous example. You
+can also use :term:`declarative configuration`, in which a Python
+:term:`python:decorator` is placed on the line above the view. Both approaches
+result in the same final configuration, thus usually, it is simply a matter of
+taste.
+
+
+Extra credit
============
#. What does the dot in ``.views`` signify?
-#. Why might ``assertIn`` be a better choice in testing the text in
- responses than ``assertEqual``?
+#. Why might ``assertIn`` be a better choice in testing the text in responses
+ than ``assertEqual``?
.. seealso:: :ref:`views_chapter`,
:ref:`view_config_chapter`, and
:ref:`debugging_view_configuration`
-
diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst
index ddd968927..3cc182d13 100644
--- a/docs/tutorials/modwsgi/index.rst
+++ b/docs/tutorials/modwsgi/index.rst
@@ -1,7 +1,7 @@
.. _modwsgi_tutorial:
Running a :app:`Pyramid` Application under ``mod_wsgi``
-==========================================================
+=======================================================
:term:`mod_wsgi` is an Apache module developed by Graham Dumpleton.
It allows :term:`WSGI` programs to be served using the Apache web
@@ -24,21 +24,15 @@ specific path information for commands and files.
system. If you do not, install Apache 2.X for your platform in
whatever manner makes sense.
+#. It is also assumed that you have satisfied the
+ :ref:`requirements-for-installing-packages`.
+
#. Once you have Apache installed, install ``mod_wsgi``. Use the
(excellent) `installation instructions
<http://code.google.com/p/modwsgi/wiki/InstallationInstructions>`_
for your platform into your system's Apache installation.
-#. Install :term:`virtualenv` into the Python which mod_wsgi will
- run using the ``easy_install`` program.
-
- .. code-block:: text
-
- $ sudo /usr/bin/easy_install-2.6 virtualenv
-
- This command may need to be performed as the root user.
-
-#. Create a :term:`virtualenv` which we'll use to install our
+#. Create a :term:`virtual environment` which we'll use to install our
application.
.. code-block:: text
@@ -46,14 +40,14 @@ specific path information for commands and files.
$ cd ~
$ mkdir modwsgi
$ cd modwsgi
- $ /usr/local/bin/virtualenv env
+ $ python3 -m venv env
-#. Install :app:`Pyramid` into the newly created virtualenv:
+#. Install :app:`Pyramid` into the newly created virtual environment:
.. code-block:: text
$ cd ~/modwsgi/env
- $ $VENV/bin/easy_install pyramid
+ $ $VENV/bin/pip install pyramid
#. Create and install your :app:`Pyramid` application. For the purposes of
this tutorial, we'll just be using the ``pyramid_starter`` application as
@@ -65,9 +59,9 @@ specific path information for commands and files.
$ cd ~/modwsgi/env
$ $VENV/bin/pcreate -s starter myapp
$ cd myapp
- $ $VENV/bin/python setup.py install
+ $ $VENV/bin/pip install -e .
-#. Within the virtualenv directory (``~/modwsgi/env``), create a
+#. Within the virtual environment directory (``~/modwsgi/env``), create a
script named ``pyramid.wsgi``. Give it these contents:
.. code-block:: python
@@ -107,7 +101,7 @@ specific path information for commands and files.
WSGIApplicationGroup %{GLOBAL}
WSGIPassAuthorization On
WSGIDaemonProcess pyramid user=chrism group=staff threads=4 \
- python-path=/Users/chrism/modwsgi/env/lib/python2.6/site-packages
+ python-path=/Users/chrism/modwsgi/env/lib/python2.7/site-packages
WSGIScriptAlias /myapp /Users/chrism/modwsgi/env/pyramid.wsgi
<Directory /Users/chrism/modwsgi/env>
@@ -131,4 +125,3 @@ serve up a :app:`Pyramid` application. See the `mod_wsgi
configuration documentation
<http://code.google.com/p/modwsgi/wiki/ConfigurationGuidelines>`_ for
more in-depth configuration information.
-
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index b0a8c155d..44097b35b 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -5,12 +5,12 @@ Adding authorization
====================
:app:`Pyramid` provides facilities for :term:`authentication` and
-::term:`authorization`. We'll make use of both features to provide security
-:to our application. Our application currently allows anyone with access to
-:the server to view, edit, and add pages to our wiki. We'll change that to
-:allow only people who are members of a *group* named ``group:editors`` to add
-:and edit wiki pages but we'll continue allowing anyone with access to the
-:server to view pages.
+:term:`authorization`. We'll make use of both features to provide security to
+our application. Our application currently allows anyone with access to the
+server to view, edit, and add pages to our wiki. We'll change that to allow
+only people who are members of a *group* named ``group:editors`` to add and
+edit wiki pages, but we'll continue allowing anyone with access to the server
+to view pages.
We will also add a login page and a logout link on all the pages. The login
page will be shown when a user is denied access to any of the views that
@@ -41,7 +41,7 @@ Access control
Add users and groups
~~~~~~~~~~~~~~~~~~~~
-Create a new ``tutorial/tutorial/security.py`` module with the
+Create a new ``tutorial/security.py`` module with the
following content:
.. literalinclude:: src/authorization/tutorial/security.py
@@ -67,7 +67,7 @@ database, but here we use "dummy" data to represent user and groups sources.
Add an ACL
~~~~~~~~~~
-Open ``tutorial/tutorial/models.py`` and add the following import
+Open ``tutorial/models.py`` and add the following import
statement at the head:
.. literalinclude:: src/authorization/tutorial/models.py
@@ -109,7 +109,7 @@ more information about what an :term:`ACL` represents.
Add authentication and authorization policies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/tutorial/__init__.py`` and add the highlighted import
+Open ``tutorial/__init__.py`` and add the highlighted import
statements:
.. literalinclude:: src/authorization/tutorial/__init__.py
@@ -142,7 +142,7 @@ machinery represented by this policy: it is required. The ``callback`` is the
Add permission declarations
~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/tutorial/views.py`` and add a ``permission='edit'`` parameter
+Open ``tutorial/views.py`` and add a ``permission='edit'`` parameter
to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
.. literalinclude:: src/authorization/tutorial/views.py
@@ -196,7 +196,7 @@ link to it. This view will clear the credentials of the logged in user and
redirect back to the front page.
Add the following import statements to the head of
-``tutorial/tutorial/views.py``:
+``tutorial/views.py``:
.. literalinclude:: src/authorization/tutorial/views.py
:lines: 6-17
@@ -236,7 +236,7 @@ it with the ``logout`` route. It will be invoked when we visit ``/logout``.
Add the ``login.pt`` Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Create ``tutorial/tutorial/templates/login.pt`` with the following content:
+Create ``tutorial/templates/login.pt`` with the following content:
.. literalinclude:: src/authorization/tutorial/templates/login.pt
:language: html
@@ -247,8 +247,8 @@ The above template is referenced in the login view that we just added in
Return a ``logged_in`` flag to the renderer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/tutorial/views.py`` again. Add a ``logged_in`` parameter to
-the return value of ``view_page()``, ``edit_page()``, and ``add_page()`` as
+Open ``tutorial/views.py`` again. Add a ``logged_in`` parameter to
+the return value of ``view_page()``, ``add_page()``, and ``edit_page()`` as
follows:
.. literalinclude:: src/authorization/tutorial/views.py
@@ -262,7 +262,7 @@ follows:
:language: python
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 75-77
+ :lines: 78-80
:emphasize-lines: 2-3
:language: python
@@ -274,8 +274,8 @@ the user is not authenticated, or a userid if the user is authenticated.
Add a "Logout" link when logged in
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/tutorial/templates/edit.pt`` and
-``tutorial/tutorial/templates/view.pt`` and add the following code as
+Open ``tutorial/templates/edit.pt`` and
+``tutorial/templates/view.pt`` and add the following code as
indicated by the highlighted lines.
.. literalinclude:: src/authorization/tutorial/templates/edit.pt
@@ -291,7 +291,7 @@ a user is not authenticated.
Reviewing our changes
---------------------
-Our ``tutorial/tutorial/__init__.py`` will look like this when we're done:
+Our ``tutorial/__init__.py`` will look like this when we're done:
.. literalinclude:: src/authorization/tutorial/__init__.py
:linenos:
@@ -300,7 +300,7 @@ Our ``tutorial/tutorial/__init__.py`` will look like this when we're done:
Only the highlighted lines need to be added or edited.
-Our ``tutorial/tutorial/models.py`` will look like this when we're done:
+Our ``tutorial/models.py`` will look like this when we're done:
.. literalinclude:: src/authorization/tutorial/models.py
:linenos:
@@ -309,7 +309,7 @@ Our ``tutorial/tutorial/models.py`` will look like this when we're done:
Only the highlighted lines need to be added or edited.
-Our ``tutorial/tutorial/views.py`` will look like this when we're done:
+Our ``tutorial/views.py`` will look like this when we're done:
.. literalinclude:: src/authorization/tutorial/views.py
:linenos:
@@ -318,7 +318,7 @@ Our ``tutorial/tutorial/views.py`` will look like this when we're done:
Only the highlighted lines need to be added or edited.
-Our ``tutorial/tutorial/templates/edit.pt`` template will look like this when
+Our ``tutorial/templates/edit.pt`` template will look like this when
we're done:
.. literalinclude:: src/authorization/tutorial/templates/edit.pt
@@ -328,7 +328,7 @@ we're done:
Only the highlighted lines need to be added or edited.
-Our ``tutorial/tutorial/templates/view.pt`` template will look like this when
+Our ``tutorial/templates/view.pt`` template will look like this when
we're done:
.. literalinclude:: src/authorization/tutorial/templates/view.pt
diff --git a/docs/tutorials/wiki/background.rst b/docs/tutorials/wiki/background.rst
index 6bbd5026e..31dcd6b53 100644
--- a/docs/tutorials/wiki/background.rst
+++ b/docs/tutorials/wiki/background.rst
@@ -1,3 +1,5 @@
+.. _wiki_background:
+
==========
Background
==========
diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst
index 0484ebf17..20bfdf754 100644
--- a/docs/tutorials/wiki/basiclayout.rst
+++ b/docs/tutorials/wiki/basiclayout.rst
@@ -1,3 +1,5 @@
+.. _wiki_basic_layout:
+
============
Basic Layout
============
@@ -12,21 +14,22 @@ Application configuration with ``__init__.py``
A directory on disk can be turned into a Python :term:`package` by containing
an ``__init__.py`` file. Even if empty, this marks a directory as a Python
-package. We use ``__init__.py`` both as a marker, indicating the directory
-in which it's contained is a package, and to contain application configuration
+package. We use ``__init__.py`` both as a marker, indicating the directory in
+which it's contained is a package, and to contain application configuration
code.
When you run the application using the ``pserve`` command using the
``development.ini`` generated configuration file, the application
-configuration points at a Setuptools *entry point* described as
+configuration points at a setuptools *entry point* described as
``egg:tutorial``. In our application, because the application's ``setup.py``
file says so, this entry point happens to be the ``main`` function within the
-file named ``__init__.py``. Let's take a look at the code and describe what
-it does:
+file named ``__init__.py``.
+
+Open ``tutorial/__init__.py``. It should already contain the following:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :linenos:
+ :language: py
#. *Lines 1-3*. Perform some dependency imports.
@@ -81,9 +84,9 @@ resource objects, each of which also happens to be a domain model object.
Here is the source for ``models.py``:
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/models.py
+ :linenos:
+ :language: python
#. *Lines 4-5*. The ``MyModel`` :term:`resource` class is implemented here.
Instances of this class are capable of being persisted in :term:`ZODB`
@@ -113,9 +116,9 @@ the URL ``http://localhost:6543/``.
Here is the source for ``views.py``:
- .. literalinclude:: src/basiclayout/tutorial/views.py
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/views.py
+ :linenos:
+ :language: python
Let's try to understand the components in this module:
@@ -169,7 +172,7 @@ The ``development.ini`` (in the tutorial :term:`project` directory, as
opposed to the tutorial :term:`package` directory) looks like this:
.. literalinclude:: src/basiclayout/development.ini
- :language: ini
+ :language: ini
Note the existence of a ``[app:main]`` section which specifies our WSGI
application. Our ZODB database settings are specified as the
diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst
index 859e902ab..73dce14d5 100644
--- a/docs/tutorials/wiki/definingmodels.rst
+++ b/docs/tutorials/wiki/definingmodels.rst
@@ -1,9 +1,11 @@
+.. _wiki_defining_the_domain_model:
+
=========================
Defining the Domain Model
=========================
-The first change we'll make to our stock pcreate-generated application will be
-to define two :term:`resource` constructors, one representing a wiki page,
+The first change we'll make to our stock ``pcreate``-generated application will
+be to define two :term:`resource` constructors, one representing a wiki page,
and another representing the wiki as a mapping of wiki page names to page
objects. We'll do this inside our ``models.py`` file.
@@ -38,8 +40,7 @@ Edit ``models.py``
or they may live in a Python subpackage of your application package named
``models``, but this is only by convention.
-Open ``tutorial/tutorial/models.py`` file and edit it to look like the
-following:
+Open ``tutorial/models.py`` file and edit it to look like the following:
.. literalinclude:: src/models/tutorial/models.py
:linenos:
diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst
index ed173a706..ac94d8059 100644
--- a/docs/tutorials/wiki/definingviews.rst
+++ b/docs/tutorials/wiki/definingviews.rst
@@ -1,3 +1,5 @@
+.. _wiki_defining_views:
+
==============
Defining Views
==============
@@ -41,7 +43,7 @@ We need to add a dependency on the ``docutils`` package to our ``tutorial``
package's ``setup.py`` file by assigning this dependency to the ``requires``
parameter in the ``setup()`` function.
-Open ``tutorial/setup.py`` and edit it to look like the following:
+Open ``setup.py`` and edit it to look like the following:
.. literalinclude:: src/views/setup.py
:linenos:
@@ -50,40 +52,44 @@ Open ``tutorial/setup.py`` and edit it to look like the following:
Only the highlighted line needs to be added.
-Running ``setup.py develop``
+
+Running ``pip install -e .``
============================
-Since a new software dependency was added, you will need to run ``python
-setup.py develop`` again inside the root of the ``tutorial`` package to obtain
-and register the newly added dependency distribution.
+Since a new software dependency was added, you will need to run ``pip install
+-e .`` again inside the root of the ``tutorial`` package to obtain and register
+the newly added dependency distribution.
Make sure your current working directory is the root of the project (the
directory in which ``setup.py`` lives) and execute the following command.
On UNIX:
-.. code-block:: text
+.. code-block:: bash
$ cd tutorial
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
On Windows:
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut> cd tutorial
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+ c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e .
Success executing this command will end with a line to the console something
-like::
+like:
+
+.. code-block:: text
+
+ Successfully installed docutils-0.12 tutorial-0.0
- Finished processing dependencies for tutorial==0.0
Adding view functions in ``views.py``
=====================================
-It's time for a major change. Open ``tutorial/tutorial/views.py`` and edit it
-to look like the following:
+It's time for a major change. Open ``tutorial/views.py`` and edit it to look
+like the following:
.. literalinclude:: src/views/tutorial/views.py
:linenos:
@@ -310,7 +316,7 @@ extension to be recognized as such.
The ``view.pt`` template
------------------------
-Create ``tutorial/tutorial/templates/view.pt`` and add the following
+Create ``tutorial/templates/view.pt`` and add the following
content:
.. literalinclude:: src/views/tutorial/templates/view.pt
@@ -329,8 +335,7 @@ wiki page. It includes:
The ``edit.pt`` template
------------------------
-Create ``tutorial/tutorial/templates/edit.pt`` and add the following
-content:
+Create ``tutorial/templates/edit.pt`` and add the following content:
.. literalinclude:: src/views/tutorial/templates/edit.pt
:linenos:
@@ -352,6 +357,7 @@ The form POSTs back to the ``save_url`` argument supplied by the view (line
See :ref:`renderer_system_values` for information about other names that
are available by default when a template is used as a renderer.
+
Static assets
-------------
@@ -367,6 +373,7 @@ subdirectories) and are just referred to by URL or by using the convenience
method ``static_url``, e.g.,
``request.static_url('<package>:static/foo.css')`` within templates.
+
Viewing the application in a browser
====================================
diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst
index 46c2a2f30..f2a02176b 100644
--- a/docs/tutorials/wiki/design.rst
+++ b/docs/tutorials/wiki/design.rst
@@ -1,3 +1,5 @@
+.. _wiki_design:
+
======
Design
======
diff --git a/docs/tutorials/wiki/distributing.rst b/docs/tutorials/wiki/distributing.rst
index fee50a1cf..c3037f396 100644
--- a/docs/tutorials/wiki/distributing.rst
+++ b/docs/tutorials/wiki/distributing.rst
@@ -1,3 +1,5 @@
+.. _wiki_distributing_your_application:
+
=============================
Distributing Your Application
=============================
@@ -5,18 +7,18 @@ Distributing Your Application
Once your application works properly, you can create a "tarball" from it by
using the ``setup.py sdist`` command. The following commands assume your
current working directory is the ``tutorial`` package we've created and that
-the parent directory of the ``tutorial`` package is a virtualenv representing
-a :app:`Pyramid` environment.
+the parent directory of the ``tutorial`` package is a virtual environment
+representing a :app:`Pyramid` environment.
On UNIX:
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/python setup.py sdist
On Windows:
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut> %VENV%\Scripts\python setup.py sdist
@@ -25,16 +27,15 @@ The output of such a command will be something like:
.. code-block:: text
running sdist
- # .. more output ..
+ # more output
creating dist
- tar -cf dist/tutorial-0.0.tar tutorial-0.0
- gzip -f9 dist/tutorial-0.0.tar
+ Creating tar archive
removing 'tutorial-0.0' (and everything under it)
Note that this command creates a tarball in the "dist" subdirectory named
``tutorial-0.0.tar.gz``. You can send this file to your friends to show them
your cool new application. They should be able to install it by pointing the
-``easy_install`` command directly at it. Or you can upload it to `PyPI
+``pip install .`` command directly at it. Or you can upload it to `PyPI
<http://pypi.python.org>`_ and share it with the rest of the world, where it
-can be downloaded via ``easy_install`` remotely like any other package people
+can be downloaded via ``pip install`` remotely like any other package people
download from PyPI.
diff --git a/docs/tutorials/wiki/index.rst b/docs/tutorials/wiki/index.rst
index 89c026dac..7808c7623 100644
--- a/docs/tutorials/wiki/index.rst
+++ b/docs/tutorials/wiki/index.rst
@@ -26,4 +26,3 @@ which corresponds to the same location if you have Pyramid sources.
authorization
tests
distributing
-
diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst
index ff5cac4c9..dbf995595 100644
--- a/docs/tutorials/wiki/installation.rst
+++ b/docs/tutorials/wiki/installation.rst
@@ -1,17 +1,19 @@
+.. _wiki_installation:
+
============
Installation
============
Before you begin
-================
+----------------
This tutorial assumes that you have already followed the steps in
-:ref:`installing_chapter`, except **do not create a virtualenv or install
-Pyramid**. Thereby you will satisfy the following requirements.
+:ref:`installing_chapter`, except **do not create a virtual environment or
+install Pyramid**. Thereby you will satisfy the following requirements.
+
+* A Python interpreter is installed on your operating system.
+* You've satisfied the :ref:`requirements-for-installing-packages`.
-* Python interpreter is installed on your operating system
-* :term:`setuptools` or :term:`distribute` is installed
-* :term:`virtualenv` is installed
Create directory to contain the project
---------------------------------------
@@ -21,257 +23,328 @@ We need a workspace for our project files.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ mkdir ~/pyramidtut
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\> mkdir pyramidtut
+
Create and use a virtual Python environment
-------------------------------------------
-Next let's create a `virtualenv` workspace for our project. We will
-use the `VENV` environment variable instead of the absolute path of the
-virtual environment.
+Next let's create a virtual environment workspace for our project. We will use
+the ``VENV`` environment variable instead of the absolute path of the virtual
+environment.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ export VENV=~/pyramidtut
- $ virtualenv $VENV
- New python executable in /home/foo/env/bin/python
- Installing setuptools.............done.
+ $ python3 -m venv $VENV
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\> set VENV=c:\pyramidtut
-Versions of Python use different paths, so you will need to adjust the
+Each version of Python uses different paths, so you will need to adjust the
path to the command for your Python version.
Python 2.7:
-.. code-block:: text
+.. code-block:: doscon
c:\> c:\Python27\Scripts\virtualenv %VENV%
-Python 3.3:
+Python 3.5:
-.. code-block:: text
+.. code-block:: doscon
+
+ c:\> c:\Python35\Scripts\python -m venv %VENV%
- c:\> c:\Python33\Scripts\virtualenv %VENV%
-Install Pyramid and tutorial dependencies into the virtual Python environment
------------------------------------------------------------------------------
+Upgrade ``pip`` and ``setuptools`` in the virtual environment
+-------------------------------------------------------------
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/easy_install docutils pyramid_tm pyramid_zodbconn \
- pyramid_debugtoolbar nose coverage
+ $ $VENV/bin/pip install --upgrade pip setuptools
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
+
+ c:\> %VENV%\Scripts\pip install --upgrade pip setuptools
+
+
+Install Pyramid into the virtual Python environment
+---------------------------------------------------
+
+On UNIX
+^^^^^^^
- c:\> %VENV%\Scripts\easy_install docutils pyramid_tm pyramid_zodbconn \
- pyramid_debugtoolbar nose coverage
+.. code-block:: bash
-Change Directory to Your Virtual Python Environment
+ $ $VENV/bin/pip install pyramid
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ c:\> %VENV%\Scripts\pip install pyramid
+
+Change directory to your virtual Python environment
---------------------------------------------------
-Change directory to the ``pyramidtut`` directory.
+Change directory to the ``pyramidtut`` directory, which is both your workspace
+and your virtual environment.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ cd pyramidtut
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\> cd pyramidtut
.. _making_a_project:
Making a project
-================
+----------------
Your next step is to create a project. For this tutorial, we will use
the :term:`scaffold` named ``zodb``, which generates an application
that uses :term:`ZODB` and :term:`traversal`.
-:app:`Pyramid` supplies a variety of scaffolds to generate sample
-projects. We will use `pcreate`—a script that comes with Pyramid to
-quickly and easily generate scaffolds, usually with a single command—to
-create the scaffold for our project.
+:app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We
+will use ``pcreate``, a script that comes with Pyramid, to create our project
+using a scaffold.
-By passing `zodb` into the `pcreate` command, the script creates
-the files needed to use ZODB. By passing in our application name
-`tutorial`, the script inserts that application name into all the
-required files.
+By passing ``zodb`` into the ``pcreate`` command, the script creates the files
+needed to use ZODB. By passing in our application name ``tutorial``, the script
+inserts that application name into all the required files.
The below instructions assume your current working directory is "pyramidtut".
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/pcreate -s zodb tutorial
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut> %VENV%\Scripts\pcreate -s zodb tutorial
-.. note:: If you are using Windows, the ``zodb``
- scaffold may not deal gracefully with installation into a
- location that contains spaces in the path. If you experience
- startup problems, try putting both the virtualenv and the project
- into directories that do not contain spaces in their paths.
+.. note:: If you are using Windows, the ``zodb`` scaffold may not deal
+ gracefully with installation into a location that contains spaces in the
+ path. If you experience startup problems, try putting both the virtual
+ environment and the project into directories that do not contain spaces in
+ their paths.
+
.. _installing_project_in_dev_mode_zodb:
Installing the project in development mode
-==========================================
+------------------------------------------
-In order to do development on the project easily, you must "register"
-the project as a development egg in your workspace using the
-``setup.py develop`` command. In order to do so, cd to the `tutorial`
-directory you created in :ref:`making_a_project`, and run the
-``setup.py develop`` command using the virtualenv Python interpreter.
+In order to do development on the project easily, you must "register" the
+project as a development egg in your workspace using the ``pip install -e .``
+command. In order to do so, change directory to the ``tutorial`` directory that
+you created in :ref:`making_a_project`, and run the ``pip install -e .``
+command using the virtual environment Python interpreter.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ cd tutorial
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut> cd tutorial
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+ c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e .
+
+The console will show ``pip`` checking for packages and installing missing
+packages. Success executing this command will show a line like the following:
+
+.. code-block:: bash
-The console will show `setup.py` checking for packages and installing
-missing packages. Success executing this command will show a line like
-the following::
+ Successfully installed BTrees-4.2.0 Chameleon-2.24 Mako-1.0.4 \
+ MarkupSafe-0.23 Pygments-2.1.3 ZConfig-3.1.0 ZEO-4.2.0b1 ZODB-4.2.0 \
+ ZODB3-3.11.0 mock-2.0.0 pbr-1.8.1 persistent-4.1.1 pyramid-chameleon-0.3 \
+ pyramid-debugtoolbar-2.4.2 pyramid-mako-1.0.2 pyramid-tm-0.12.1 \
+ pyramid-zodbconn-0.7 six-1.10.0 transaction-1.4.4 tutorial waitress-0.8.10 \
+ zc.lockfile-1.1.0 zdaemon-4.1.0 zodbpickle-0.6.0 zodburi-2.0
+
+
+.. _install-testing-requirements_zodb:
+
+Install testing requirements
+----------------------------
+
+In order to run tests, we need to install the testing requirements. This is
+done through our project's ``setup.py`` file, in the ``tests_require`` and
+``extras_require`` stanzas, and by issuing the command below for your
+operating system.
+
+.. literalinclude:: src/installation/setup.py
+ :language: python
+ :linenos:
+ :lineno-start: 22
+ :lines: 22-26
+
+.. literalinclude:: src/installation/setup.py
+ :language: python
+ :linenos:
+ :lineno-start: 45
+ :lines: 45-47
+
+On UNIX
+^^^^^^^
+
+.. code-block:: bash
+
+ $ $VENV/bin/pip install -e ".[testing]"
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e ".[testing]"
- Finished processing dependencies for tutorial==0.0
.. _running_tests:
Run the tests
-=============
+-------------
-After you've installed the project in development mode, you may run
-the tests for the project.
+After you've installed the project in development mode as well as the testing
+requirements, you may run the tests for the project.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/python setup.py test -q
+ $ $VENV/bin/py.test tutorial/tests.py -q
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
+
+ c:\pyramidtut\tutorial> %VENV%\Scripts\py.test tutorial\tests.py -q
+
+For a successful test run, you should see output that ends like this:
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
+.. code-block:: bash
-For a successful test run, you should see output that ends like this::
+ .
+ 1 passed in 0.24 seconds
- .
- ----------------------------------------------------------------------
- Ran 1 test in 0.094s
-
- OK
Expose test coverage information
-================================
+--------------------------------
-You can run the ``nosetests`` command to see test coverage
-information. This runs the tests in the same way that ``setup.py
-test`` does but provides additional "coverage" information, exposing
-which lines of your project are "covered" (or not covered) by the
+You can run the ``py.test`` command to see test coverage information. This
+runs the tests in the same way that ``py.test`` does, but provides additional
+"coverage" information, exposing which lines of your project are covered by the
tests.
+We've already installed the ``pytest-cov`` package into our virtual
+environment, so we can run the tests with coverage.
+
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage
+ $ $VENV/bin/py.test --cov=tutorial --cov-report=term-missing tutorial/tests.py
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
+
+ c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial \
+ --cov-report=term-missing tutorial\tests.py
+
+If successful, you will see output something like this:
- c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \
- --cover-erase --with-coverage
+.. code-block:: bash
-If successful, you will see output something like this::
+ ======================== test session starts ========================
+ platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile:
+ plugins: cov-2.2.1
+ collected 1 items
- .
- Name Stmts Miss Cover Missing
- --------------------------------------------------
- tutorial.py 12 7 42% 7-8, 14-18
- tutorial/models.py 10 6 40% 9-14
- tutorial/views.py 4 0 100%
- --------------------------------------------------
- TOTAL 26 13 50%
- ----------------------------------------------------------------------
- Ran 1 test in 0.392s
+ tutorial/tests.py .
+ ------------------ coverage: platform Python 3.5.1 ------------------
+ Name Stmts Miss Cover Missing
+ ----------------------------------------------------
+ tutorial/__init__.py 12 7 42% 7-8, 14-18
+ tutorial/models.py 10 6 40% 9-14
+ tutorial/tests.py 12 0 100%
+ tutorial/views.py 4 0 100%
+ ----------------------------------------------------
+ TOTAL 38 13 66%
- OK
+ ===================== 1 passed in 0.31 seconds ======================
+
+Our package doesn't quite have 100% test coverage.
-Looks like our package doesn't quite have 100% test coverage.
.. _wiki-start-the-application:
Start the application
-=====================
+---------------------
Start the application.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/pserve development.ini --reload
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload
@@ -280,34 +353,38 @@ On Windows
Your OS firewall, if any, may pop up a dialog asking for authorization
to allow python to accept incoming network connections.
-If successful, you will see something like this on your console::
+If successful, you will see something like this on your console:
+
+.. code-block:: text
Starting subprocess with file monitor
Starting server in PID 95736.
- serving on http://0.0.0.0:6543
+ serving on http://127.0.0.1:6543
This means the server is ready to accept requests.
+
Visit the application in a browser
-==================================
+----------------------------------
-In a browser, visit `http://localhost:6543/ <http://localhost:6543>`_. You
-will see the generated application's default page.
+In a browser, visit http://localhost:6543/. You will see the generated
+application's default page.
One thing you'll notice is the "debug toolbar" icon on right hand side of the
page. You can read more about the purpose of the icon at
:ref:`debug_toolbar`. It allows you to get information about your
application while you develop.
+
Decisions the ``zodb`` scaffold has made for you
-================================================
+------------------------------------------------
Creating a project using the ``zodb`` scaffold makes the following
assumptions:
-- you are willing to use :term:`ZODB` as persistent storage
+- You are willing to use :term:`ZODB` as persistent storage.
-- you are willing to use :term:`traversal` to map URLs to code
+- You are willing to use :term:`traversal` to map URLs to code.
.. note::
diff --git a/docs/tutorials/wiki/src/authorization/CHANGES.txt b/docs/tutorials/wiki/src/authorization/CHANGES.txt
index e14f633ab..35a34f332 100644
--- a/docs/tutorials/wiki/src/authorization/CHANGES.txt
+++ b/docs/tutorials/wiki/src/authorization/CHANGES.txt
@@ -1,5 +1,4 @@
0.0
---
-- Initial version
-
+- Initial version
diff --git a/docs/tutorials/wiki/src/authorization/README.txt b/docs/tutorials/wiki/src/authorization/README.txt
index d41f7f90f..dcb3605b8 100644
--- a/docs/tutorials/wiki/src/authorization/README.txt
+++ b/docs/tutorials/wiki/src/authorization/README.txt
@@ -1,4 +1,12 @@
tutorial README
+==================
+Getting Started
+---------------
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/authorization/development.ini b/docs/tutorials/wiki/src/authorization/development.ini
index 72bd22e54..6bf4b198e 100644
--- a/docs/tutorials/wiki/src/authorization/development.ini
+++ b/docs/tutorials/wiki/src/authorization/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,12 +29,12 @@ zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -62,4 +62,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/authorization/production.ini b/docs/tutorials/wiki/src/authorization/production.ini
index d9bf27c42..4e9892e7b 100644
--- a/docs/tutorials/wiki/src/authorization/production.ini
+++ b/docs/tutorials/wiki/src/authorization/production.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,7 +29,7 @@ port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -57,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py
index e2e96379d..beeed75c9 100644
--- a/docs/tutorials/wiki/src/authorization/setup.py
+++ b/docs/tutorials/wiki/src/authorization/setup.py
@@ -20,16 +20,22 @@ requires = [
'docutils',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -37,9 +43,10 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
- tests_require=requires,
- test_suite="tutorial",
entry_points="""\
[paste.app_factory]
main = tutorial:main
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/models.py b/docs/tutorials/wiki/src/authorization/tutorial/models.py
index 582ff0d7e..38fdd2dfc 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/models.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/models.py
@@ -17,7 +17,7 @@ class Page(Persistent):
self.data = data
def appmaker(zodb_root):
- if not 'app_root' in zodb_root:
+ if 'app_root' not in zodb_root:
app_root = Wiki()
frontpage = Page('This is the front page')
app_root['FrontPage'] = frontpage
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/theme.min.css b/docs/tutorials/wiki/src/authorization/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt
index 1b30f42b6..f8cbe2e2c 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt
@@ -34,14 +34,15 @@
<div class="col-md-10">
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
</div>
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/tests.py b/docs/tutorials/wiki/src/authorization/tutorial/tests.py
index 0b9046d47..40f3c47af 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/tests.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/tests.py
@@ -2,122 +2,16 @@ import unittest
from pyramid import testing
-class PageModelTests(unittest.TestCase):
- def _getTargetClass(self):
- from .models import Page
- return Page
+class ViewTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
- def _makeOne(self, data=u'some data'):
- return self._getTargetClass()(data=data)
+ def tearDown(self):
+ testing.tearDown()
- def test_constructor(self):
- instance = self._makeOne()
- self.assertEqual(instance.data, u'some data')
-
-class WikiModelTests(unittest.TestCase):
-
- def _getTargetClass(self):
- from .models import Wiki
- return Wiki
-
- def _makeOne(self):
- return self._getTargetClass()()
-
- def test_it(self):
- wiki = self._makeOne()
- self.assertEqual(wiki.__parent__, None)
- self.assertEqual(wiki.__name__, None)
-
-class AppmakerTests(unittest.TestCase):
-
- def _callFUT(self, zodb_root):
- from .models import appmaker
- return appmaker(zodb_root)
-
- def test_it(self):
- root = {}
- self._callFUT(root)
- self.assertEqual(root['app_root']['FrontPage'].data,
- 'This is the front page')
-
-class ViewWikiTests(unittest.TestCase):
- def test_it(self):
- from .views import view_wiki
- context = testing.DummyResource()
- request = testing.DummyRequest()
- response = view_wiki(context, request)
- self.assertEqual(response.location, 'http://example.com/FrontPage')
-
-class ViewPageTests(unittest.TestCase):
- def _callFUT(self, context, request):
- from .views import view_page
- return view_page(context, request)
-
- def test_it(self):
- wiki = testing.DummyResource()
- wiki['IDoExist'] = testing.DummyResource()
- context = testing.DummyResource(data='Hello CruelWorld IDoExist')
- context.__parent__ = wiki
- context.__name__ = 'thepage'
- request = testing.DummyRequest()
- info = self._callFUT(context, request)
- self.assertEqual(info['page'], context)
- self.assertEqual(
- info['content'],
- '<div class="document">\n'
- '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
- 'CruelWorld</a> '
- '<a href="http://example.com/IDoExist/">'
- 'IDoExist</a>'
- '</p>\n</div>\n')
- self.assertEqual(info['edit_url'],
- 'http://example.com/thepage/edit_page')
-
-
-class AddPageTests(unittest.TestCase):
- def _callFUT(self, context, request):
- from .views import add_page
- return add_page(context, request)
-
- def test_it_notsubmitted(self):
- context = testing.DummyResource()
+ def test_my_view(self):
+ from .views import my_view
request = testing.DummyRequest()
- request.subpath = ['AnotherPage']
- info = self._callFUT(context, request)
- self.assertEqual(info['page'].data,'')
- self.assertEqual(
- info['save_url'],
- request.resource_url(context, 'add_page', 'AnotherPage'))
-
- def test_it_submitted(self):
- context = testing.DummyResource()
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- request.subpath = ['AnotherPage']
- self._callFUT(context, request)
- page = context['AnotherPage']
- self.assertEqual(page.data, 'Hello yo!')
- self.assertEqual(page.__name__, 'AnotherPage')
- self.assertEqual(page.__parent__, context)
-
-class EditPageTests(unittest.TestCase):
- def _callFUT(self, context, request):
- from .views import edit_page
- return edit_page(context, request)
-
- def test_it_notsubmitted(self):
- context = testing.DummyResource()
- request = testing.DummyRequest()
- info = self._callFUT(context, request)
- self.assertEqual(info['page'], context)
- self.assertEqual(info['save_url'],
- request.resource_url(context, 'edit_page'))
-
- def test_it_submitted(self):
- context = testing.DummyResource()
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- response = self._callFUT(context, request)
- self.assertEqual(response.location, 'http://example.com/')
- self.assertEqual(context.data, 'Hello yo!')
+ info = my_view(request)
+ self.assertEqual(info['project'], 'tutorial')
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py
index 62e96e0e7..c271d2cc1 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/views.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py
@@ -37,15 +37,15 @@ def view_page(context, request):
view_url = request.resource_url(page)
return '<a href="%s">%s</a>' % (view_url, word)
else:
- add_url = request.application_url + '/add_page/' + word
+ add_url = request.application_url + '/add_page/' + word
return '<a href="%s">%s</a>' % (add_url, word)
content = publish_parts(context.data, writer_name='html')['html_body']
content = wikiwords.sub(check, content)
edit_url = request.resource_url(context, 'edit_page')
- return dict(page = context, content = content, edit_url = edit_url,
- logged_in = request.authenticated_userid)
+ return dict(page=context, content=content, edit_url=edit_url,
+ logged_in=request.authenticated_userid)
@view_config(name='add_page', context='.models.Wiki',
renderer='templates/edit.pt',
@@ -58,7 +58,7 @@ def add_page(context, request):
page.__name__ = pagename
page.__parent__ = context
context[pagename] = page
- return HTTPFound(location = request.resource_url(page))
+ return HTTPFound(location=request.resource_url(page))
save_url = request.resource_url(context, 'add_page', pagename)
page = Page('')
page.__name__ = pagename
@@ -73,7 +73,7 @@ def add_page(context, request):
def edit_page(context, request):
if 'form.submitted' in request.params:
context.data = request.params['body']
- return HTTPFound(location = request.resource_url(context))
+ return HTTPFound(location=request.resource_url(context))
return dict(page=context,
save_url=request.resource_url(context, 'edit_page'),
@@ -86,7 +86,7 @@ def login(request):
login_url = request.resource_url(request.context, 'login')
referrer = request.url
if referrer == login_url:
- referrer = '/' # never use the login form itself as came_from
+ referrer = '/' # never use the login form itself as came_from
came_from = request.params.get('came_from', referrer)
message = ''
login = ''
@@ -96,20 +96,21 @@ def login(request):
password = request.params['password']
if USERS.get(login) == password:
headers = remember(request, login)
- return HTTPFound(location = came_from,
- headers = headers)
+ return HTTPFound(location=came_from,
+ headers=headers)
message = 'Failed login'
return dict(
- message = message,
- url = request.application_url + '/login',
- came_from = came_from,
- login = login,
- password = password,
- )
+ message=message,
+ url=request.application_url + '/login',
+ came_from=came_from,
+ login=login,
+ password=password,
+ )
+
@view_config(context='.models.Wiki', name='logout')
def logout(request):
headers = forget(request)
- return HTTPFound(location = request.resource_url(request.context),
- headers = headers)
+ return HTTPFound(location=request.resource_url(request.context),
+ headers=headers)
diff --git a/docs/tutorials/wiki/src/basiclayout/README.txt b/docs/tutorials/wiki/src/basiclayout/README.txt
index d41f7f90f..dcb3605b8 100644
--- a/docs/tutorials/wiki/src/basiclayout/README.txt
+++ b/docs/tutorials/wiki/src/basiclayout/README.txt
@@ -1,4 +1,12 @@
tutorial README
+==================
+Getting Started
+---------------
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/basiclayout/development.ini b/docs/tutorials/wiki/src/basiclayout/development.ini
index 72bd22e54..6bf4b198e 100644
--- a/docs/tutorials/wiki/src/basiclayout/development.ini
+++ b/docs/tutorials/wiki/src/basiclayout/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,12 +29,12 @@ zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -62,4 +62,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/basiclayout/production.ini b/docs/tutorials/wiki/src/basiclayout/production.ini
index d9bf27c42..4e9892e7b 100644
--- a/docs/tutorials/wiki/src/basiclayout/production.ini
+++ b/docs/tutorials/wiki/src/basiclayout/production.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,7 +29,7 @@ port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -57,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/basiclayout/setup.py b/docs/tutorials/wiki/src/basiclayout/setup.py
index 58a454f80..46b395568 100644
--- a/docs/tutorials/wiki/src/basiclayout/setup.py
+++ b/docs/tutorials/wiki/src/basiclayout/setup.py
@@ -19,16 +19,22 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -36,9 +42,10 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
- tests_require=requires,
- test_suite="tutorial",
entry_points="""\
[paste.app_factory]
main = tutorial:main
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki/src/basiclayout/tutorial/models.py
index a94b36ef4..e5aa3e9f7 100644
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/models.py
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/models.py
@@ -6,7 +6,7 @@ class MyModel(PersistentMapping):
def appmaker(zodb_root):
- if not 'app_root' in zodb_root:
+ if 'app_root' not in zodb_root:
app_root = MyModel()
zodb_root['app_root'] = app_root
import transaction
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.min.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt
index 1b30f42b6..f8cbe2e2c 100644
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt
@@ -34,14 +34,15 @@
<div class="col-md-10">
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
</div>
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py
index 7f6523c66..40f3c47af 100644
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py
@@ -2,6 +2,7 @@ import unittest
from pyramid import testing
+
class ViewTests(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
diff --git a/docs/tutorials/wiki/src/installation/CHANGES.txt b/docs/tutorials/wiki/src/installation/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki/src/installation/MANIFEST.in b/docs/tutorials/wiki/src/installation/MANIFEST.in
new file mode 100644
index 000000000..81beba1b1
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki/src/installation/README.txt b/docs/tutorials/wiki/src/installation/README.txt
new file mode 100644
index 000000000..dcb3605b8
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/README.txt
@@ -0,0 +1,12 @@
+tutorial README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
+
diff --git a/docs/tutorials/wiki/src/installation/development.ini b/docs/tutorials/wiki/src/installation/development.ini
new file mode 100644
index 000000000..6bf4b198e
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/development.ini
@@ -0,0 +1,65 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+ pyramid_zodbconn
+ pyramid_tm
+
+tm.attempts = 3
+zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+host = 127.0.0.1
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_tutorial]
+level = DEBUG
+handlers =
+qualname = tutorial
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/installation/production.ini b/docs/tutorials/wiki/src/installation/production.ini
new file mode 100644
index 000000000..4e9892e7b
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/production.ini
@@ -0,0 +1,60 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_tm
+ pyramid_zodbconn
+
+tm.attempts = 3
+zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+host = 0.0.0.0
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_tutorial]
+level = WARN
+handlers =
+qualname = tutorial
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/installation/setup.py b/docs/tutorials/wiki/src/installation/setup.py
new file mode 100644
index 000000000..46b395568
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/setup.py
@@ -0,0 +1,53 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.txt')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'pyramid',
+ 'pyramid_chameleon',
+ 'pyramid_debugtoolbar',
+ 'pyramid_tm',
+ 'pyramid_zodbconn',
+ 'transaction',
+ 'ZODB3',
+ 'waitress',
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web pylons pyramid',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
+ install_requires=requires,
+ entry_points="""\
+ [paste.app_factory]
+ main = tutorial:main
+ """,
+ )
diff --git a/docs/tutorials/wiki/src/installation/tutorial/__init__.py b/docs/tutorials/wiki/src/installation/tutorial/__init__.py
new file mode 100644
index 000000000..f2a86df47
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/__init__.py
@@ -0,0 +1,18 @@
+from pyramid.config import Configurator
+from pyramid_zodbconn import get_connection
+from .models import appmaker
+
+
+def root_factory(request):
+ conn = get_connection(request)
+ return appmaker(conn.root())
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(root_factory=root_factory, settings=settings)
+ config.include('pyramid_chameleon')
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki/src/installation/tutorial/models.py b/docs/tutorials/wiki/src/installation/tutorial/models.py
new file mode 100644
index 000000000..e5aa3e9f7
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/models.py
@@ -0,0 +1,14 @@
+from persistent.mapping import PersistentMapping
+
+
+class MyModel(PersistentMapping):
+ __parent__ = __name__ = None
+
+
+def appmaker(zodb_root):
+ if 'app_root' not in zodb_root:
+ app_root = MyModel()
+ zodb_root['app_root'] = app_root
+ import transaction
+ transaction.commit()
+ return zodb_root['app_root']
diff --git a/docs/tutorials/wiki/src/installation/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/installation/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/installation/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/installation/tutorial/static/pyramid.png
new file mode 100644
index 000000000..4ab837be9
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/installation/tutorial/static/theme.css b/docs/tutorials/wiki/src/installation/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/static/theme.css
@@ -0,0 +1,154 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+body {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #ffffff;
+ background: #bc2131;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+}
+p {
+ font-weight: 300;
+}
+.font-normal {
+ font-weight: 400;
+}
+.font-semi-bold {
+ font-weight: 600;
+}
+.font-bold {
+ font-weight: 700;
+}
+.starter-template {
+ margin-top: 250px;
+}
+.starter-template .content {
+ margin-left: 10px;
+}
+.starter-template .content h1 {
+ margin-top: 10px;
+ font-size: 60px;
+}
+.starter-template .content h1 .smaller {
+ font-size: 40px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead {
+ font-size: 25px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead .font-normal {
+ color: #ffffff;
+}
+.starter-template .links {
+ float: right;
+ right: 0;
+ margin-top: 125px;
+}
+.starter-template .links ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+}
+.starter-template .links ul li {
+ list-style: none;
+ display: inline;
+ margin: 0 10px;
+}
+.starter-template .links ul li:first-child {
+ margin-left: 0;
+}
+.starter-template .links ul li:last-child {
+ margin-right: 0;
+}
+.starter-template .links ul li.current-version {
+ color: #f2b7bd;
+ font-weight: 400;
+}
+.starter-template .links ul li a, a {
+ color: #f2b7bd;
+ text-decoration: underline;
+}
+.starter-template .links ul li a:hover, a:hover {
+ color: #ffffff;
+ text-decoration: underline;
+}
+.starter-template .links ul li .icon-muted {
+ color: #eb8b95;
+ margin-right: 5px;
+}
+.starter-template .links ul li:hover .icon-muted {
+ color: #ffffff;
+}
+.starter-template .copyright {
+ margin-top: 10px;
+ font-size: 0.9em;
+ color: #f2b7bd;
+ text-transform: lowercase;
+ float: right;
+ right: 0;
+}
+@media (max-width: 1199px) {
+ .starter-template .content h1 {
+ font-size: 45px;
+ }
+ .starter-template .content h1 .smaller {
+ font-size: 30px;
+ }
+ .starter-template .content .lead {
+ font-size: 20px;
+ }
+}
+@media (max-width: 991px) {
+ .starter-template {
+ margin-top: 0;
+ }
+ .starter-template .logo {
+ margin: 40px auto;
+ }
+ .starter-template .content {
+ margin-left: 0;
+ text-align: center;
+ }
+ .starter-template .content h1 {
+ margin-bottom: 20px;
+ }
+ .starter-template .links {
+ float: none;
+ text-align: center;
+ margin-top: 60px;
+ }
+ .starter-template .copyright {
+ float: none;
+ text-align: center;
+ }
+}
+@media (max-width: 767px) {
+ .starter-template .content h1 .smaller {
+ font-size: 25px;
+ display: block;
+ }
+ .starter-template .content .lead {
+ font-size: 16px;
+ }
+ .starter-template .links {
+ margin-top: 40px;
+ }
+ .starter-template .links ul li {
+ display: block;
+ margin: 0;
+ }
+ .starter-template .links ul li .icon-muted {
+ display: none;
+ }
+ .starter-template .copyright {
+ margin-top: 20px;
+ }
+}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/installation/tutorial/templates/mytemplate.pt
index c9b0cec21..f8cbe2e2c 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki/src/installation/tutorial/templates/mytemplate.pt
@@ -8,7 +8,7 @@
<meta name="author" content="Pylons Project">
<link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
- <title>Alchemy Scaffold for The Pyramid Web Framework</title>
+ <title>ZODB Scaffold for The Pyramid Web Framework</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
@@ -33,15 +33,16 @@
</div>
<div class="col-md-10">
<div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
</div>
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki/src/installation/tutorial/tests.py b/docs/tutorials/wiki/src/installation/tutorial/tests.py
new file mode 100644
index 000000000..40f3c47af
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/tests.py
@@ -0,0 +1,17 @@
+import unittest
+
+from pyramid import testing
+
+
+class ViewTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_my_view(self):
+ from .views import my_view
+ request = testing.DummyRequest()
+ info = my_view(request)
+ self.assertEqual(info['project'], 'tutorial')
diff --git a/docs/tutorials/wiki/src/installation/tutorial/views.py b/docs/tutorials/wiki/src/installation/tutorial/views.py
new file mode 100644
index 000000000..628ce15ed
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/views.py
@@ -0,0 +1,7 @@
+from pyramid.view import view_config
+from .models import MyModel
+
+
+@view_config(context=MyModel, renderer='templates/mytemplate.pt')
+def my_view(request):
+ return {'project': 'tutorial'}
diff --git a/docs/tutorials/wiki/src/models/CHANGES.txt b/docs/tutorials/wiki/src/models/CHANGES.txt
index ffa255da8..35a34f332 100644
--- a/docs/tutorials/wiki/src/models/CHANGES.txt
+++ b/docs/tutorials/wiki/src/models/CHANGES.txt
@@ -1,4 +1,4 @@
0.0
---
-- Initial version
+- Initial version
diff --git a/docs/tutorials/wiki/src/models/README.txt b/docs/tutorials/wiki/src/models/README.txt
index d41f7f90f..dcb3605b8 100644
--- a/docs/tutorials/wiki/src/models/README.txt
+++ b/docs/tutorials/wiki/src/models/README.txt
@@ -1,4 +1,12 @@
tutorial README
+==================
+Getting Started
+---------------
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/models/development.ini b/docs/tutorials/wiki/src/models/development.ini
index 72bd22e54..6bf4b198e 100644
--- a/docs/tutorials/wiki/src/models/development.ini
+++ b/docs/tutorials/wiki/src/models/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,12 +29,12 @@ zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -62,4 +62,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/models/production.ini b/docs/tutorials/wiki/src/models/production.ini
index d9bf27c42..4e9892e7b 100644
--- a/docs/tutorials/wiki/src/models/production.ini
+++ b/docs/tutorials/wiki/src/models/production.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,7 +29,7 @@ port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -57,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/models/setup.py b/docs/tutorials/wiki/src/models/setup.py
index 58a454f80..46b395568 100644
--- a/docs/tutorials/wiki/src/models/setup.py
+++ b/docs/tutorials/wiki/src/models/setup.py
@@ -19,16 +19,22 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -36,9 +42,10 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
- tests_require=requires,
- test_suite="tutorial",
entry_points="""\
[paste.app_factory]
main = tutorial:main
diff --git a/docs/tutorials/wiki/src/models/tutorial/models.py b/docs/tutorials/wiki/src/models/tutorial/models.py
index 9761856c6..aa907aee5 100644
--- a/docs/tutorials/wiki/src/models/tutorial/models.py
+++ b/docs/tutorials/wiki/src/models/tutorial/models.py
@@ -10,7 +10,7 @@ class Page(Persistent):
self.data = data
def appmaker(zodb_root):
- if not 'app_root' in zodb_root:
+ if 'app_root' not in zodb_root:
app_root = Wiki()
frontpage = Page('This is the front page')
app_root['FrontPage'] = frontpage
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/theme.min.css b/docs/tutorials/wiki/src/models/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt
index 1b30f42b6..f8cbe2e2c 100644
--- a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt
@@ -34,14 +34,15 @@
<div class="col-md-10">
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
</div>
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki/src/models/tutorial/tests.py b/docs/tutorials/wiki/src/models/tutorial/tests.py
index 0c5f99575..40f3c47af 100644
--- a/docs/tutorials/wiki/src/models/tutorial/tests.py
+++ b/docs/tutorials/wiki/src/models/tutorial/tests.py
@@ -2,50 +2,6 @@ import unittest
from pyramid import testing
-class PageModelTests(unittest.TestCase):
-
- def _getTargetClass(self):
- from .models import Page
- return Page
-
- def _makeOne(self, data=u'some data'):
- return self._getTargetClass()(data=data)
-
- def test_constructor(self):
- instance = self._makeOne()
- self.assertEqual(instance.data, u'some data')
-
-class WikiModelTests(unittest.TestCase):
-
- def _getTargetClass(self):
- from .models import Wiki
- return Wiki
-
- def _makeOne(self):
- return self._getTargetClass()()
-
- def test_it(self):
- wiki = self._makeOne()
- self.assertEqual(wiki.__parent__, None)
- self.assertEqual(wiki.__name__, None)
-
-class AppmakerTests(unittest.TestCase):
-
- def _callFUT(self, zodb_root):
- from .models import appmaker
- return appmaker(zodb_root)
-
- def test_no_app_root(self):
- root = {}
- self._callFUT(root)
- self.assertEqual(root['app_root']['FrontPage'].data,
- 'This is the front page')
-
- def test_w_app_root(self):
- app_root = object()
- root = {'app_root': app_root}
- self._callFUT(root)
- self.assertTrue(root['app_root'] is app_root)
class ViewTests(unittest.TestCase):
def setUp(self):
diff --git a/docs/tutorials/wiki/src/tests/CHANGES.txt b/docs/tutorials/wiki/src/tests/CHANGES.txt
index e14f633ab..35a34f332 100644
--- a/docs/tutorials/wiki/src/tests/CHANGES.txt
+++ b/docs/tutorials/wiki/src/tests/CHANGES.txt
@@ -1,5 +1,4 @@
0.0
---
-- Initial version
-
+- Initial version
diff --git a/docs/tutorials/wiki/src/tests/README.txt b/docs/tutorials/wiki/src/tests/README.txt
index d41f7f90f..dcb3605b8 100644
--- a/docs/tutorials/wiki/src/tests/README.txt
+++ b/docs/tutorials/wiki/src/tests/README.txt
@@ -1,4 +1,12 @@
tutorial README
+==================
+Getting Started
+---------------
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/tests/development.ini b/docs/tutorials/wiki/src/tests/development.ini
index 72bd22e54..6bf4b198e 100644
--- a/docs/tutorials/wiki/src/tests/development.ini
+++ b/docs/tutorials/wiki/src/tests/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,12 +29,12 @@ zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -62,4 +62,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/tests/production.ini b/docs/tutorials/wiki/src/tests/production.ini
index d9bf27c42..4e9892e7b 100644
--- a/docs/tutorials/wiki/src/tests/production.ini
+++ b/docs/tutorials/wiki/src/tests/production.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,7 +29,7 @@ port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -57,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py
index b67b702cf..beeed75c9 100644
--- a/docs/tutorials/wiki/src/tests/setup.py
+++ b/docs/tutorials/wiki/src/tests/setup.py
@@ -18,7 +18,12 @@ requires = [
'ZODB3',
'waitress',
'docutils',
- 'WebTest', # add this
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
]
setup(name='tutorial',
@@ -26,11 +31,11 @@ setup(name='tutorial',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -38,9 +43,10 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
- tests_require=requires,
- test_suite="tutorial",
entry_points="""\
[paste.app_factory]
main = tutorial:main
diff --git a/docs/tutorials/wiki/src/tests/tutorial/__init__.py b/docs/tutorials/wiki/src/tests/tutorial/__init__.py
index bd3c5619f..39b94abd1 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/__init__.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/__init__.py
@@ -19,9 +19,9 @@ def main(global_config, **settings):
'sosecret', callback=groupfinder, hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()
config = Configurator(root_factory=root_factory, settings=settings)
- config.include('pyramid_chameleon')
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
+ config.include('pyramid_chameleon')
config.add_static_view('static', 'static', cache_max_age=3600)
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki/src/tests/tutorial/models.py b/docs/tutorials/wiki/src/tests/tutorial/models.py
index 582ff0d7e..38fdd2dfc 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/models.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/models.py
@@ -17,7 +17,7 @@ class Page(Persistent):
self.data = data
def appmaker(zodb_root):
- if not 'app_root' in zodb_root:
+ if 'app_root' not in zodb_root:
app_root = Wiki()
frontpage = Page('This is the front page')
app_root['FrontPage'] = frontpage
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/theme.min.css b/docs/tutorials/wiki/src/tests/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt
index 1b30f42b6..f8cbe2e2c 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt
@@ -34,14 +34,15 @@
<div class="col-md-10">
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
</div>
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py
index 5add04c20..04beaea44 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/tests.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py
@@ -164,6 +164,10 @@ class FunctionalTests(unittest.TestCase):
res = self.testapp.get('/SomePage', status=404)
self.assertTrue(b'Not Found' in res.body)
+ def test_referrer_is_login(self):
+ res = self.testapp.get('/login', status=200)
+ self.assertTrue(b'name="came_from" value="/"' in res.body)
+
def test_successful_log_in(self):
res = self.testapp.get( self.viewer_login, status=302)
self.assertEqual(res.location, 'http://localhost/FrontPage')
diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py
index 62e96e0e7..c271d2cc1 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/views.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/views.py
@@ -37,15 +37,15 @@ def view_page(context, request):
view_url = request.resource_url(page)
return '<a href="%s">%s</a>' % (view_url, word)
else:
- add_url = request.application_url + '/add_page/' + word
+ add_url = request.application_url + '/add_page/' + word
return '<a href="%s">%s</a>' % (add_url, word)
content = publish_parts(context.data, writer_name='html')['html_body']
content = wikiwords.sub(check, content)
edit_url = request.resource_url(context, 'edit_page')
- return dict(page = context, content = content, edit_url = edit_url,
- logged_in = request.authenticated_userid)
+ return dict(page=context, content=content, edit_url=edit_url,
+ logged_in=request.authenticated_userid)
@view_config(name='add_page', context='.models.Wiki',
renderer='templates/edit.pt',
@@ -58,7 +58,7 @@ def add_page(context, request):
page.__name__ = pagename
page.__parent__ = context
context[pagename] = page
- return HTTPFound(location = request.resource_url(page))
+ return HTTPFound(location=request.resource_url(page))
save_url = request.resource_url(context, 'add_page', pagename)
page = Page('')
page.__name__ = pagename
@@ -73,7 +73,7 @@ def add_page(context, request):
def edit_page(context, request):
if 'form.submitted' in request.params:
context.data = request.params['body']
- return HTTPFound(location = request.resource_url(context))
+ return HTTPFound(location=request.resource_url(context))
return dict(page=context,
save_url=request.resource_url(context, 'edit_page'),
@@ -86,7 +86,7 @@ def login(request):
login_url = request.resource_url(request.context, 'login')
referrer = request.url
if referrer == login_url:
- referrer = '/' # never use the login form itself as came_from
+ referrer = '/' # never use the login form itself as came_from
came_from = request.params.get('came_from', referrer)
message = ''
login = ''
@@ -96,20 +96,21 @@ def login(request):
password = request.params['password']
if USERS.get(login) == password:
headers = remember(request, login)
- return HTTPFound(location = came_from,
- headers = headers)
+ return HTTPFound(location=came_from,
+ headers=headers)
message = 'Failed login'
return dict(
- message = message,
- url = request.application_url + '/login',
- came_from = came_from,
- login = login,
- password = password,
- )
+ message=message,
+ url=request.application_url + '/login',
+ came_from=came_from,
+ login=login,
+ password=password,
+ )
+
@view_config(context='.models.Wiki', name='logout')
def logout(request):
headers = forget(request)
- return HTTPFound(location = request.resource_url(request.context),
- headers = headers)
+ return HTTPFound(location=request.resource_url(request.context),
+ headers=headers)
diff --git a/docs/tutorials/wiki/src/views/CHANGES.txt b/docs/tutorials/wiki/src/views/CHANGES.txt
index 1544cf53b..35a34f332 100644
--- a/docs/tutorials/wiki/src/views/CHANGES.txt
+++ b/docs/tutorials/wiki/src/views/CHANGES.txt
@@ -1,3 +1,4 @@
-0.1
+0.0
+---
- Initial version
+- Initial version
diff --git a/docs/tutorials/wiki/src/views/README.txt b/docs/tutorials/wiki/src/views/README.txt
index d41f7f90f..dcb3605b8 100644
--- a/docs/tutorials/wiki/src/views/README.txt
+++ b/docs/tutorials/wiki/src/views/README.txt
@@ -1,4 +1,12 @@
tutorial README
+==================
+Getting Started
+---------------
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/views/development.ini b/docs/tutorials/wiki/src/views/development.ini
index 72bd22e54..6bf4b198e 100644
--- a/docs/tutorials/wiki/src/views/development.ini
+++ b/docs/tutorials/wiki/src/views/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,12 +29,12 @@ zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -62,4 +62,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/views/production.ini b/docs/tutorials/wiki/src/views/production.ini
index d9bf27c42..4e9892e7b 100644
--- a/docs/tutorials/wiki/src/views/production.ini
+++ b/docs/tutorials/wiki/src/views/production.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -29,7 +29,7 @@ port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -57,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/views/setup.py b/docs/tutorials/wiki/src/views/setup.py
index e2e96379d..beeed75c9 100644
--- a/docs/tutorials/wiki/src/views/setup.py
+++ b/docs/tutorials/wiki/src/views/setup.py
@@ -20,16 +20,22 @@ requires = [
'docutils',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -37,9 +43,10 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
- tests_require=requires,
- test_suite="tutorial",
entry_points="""\
[paste.app_factory]
main = tutorial:main
diff --git a/docs/tutorials/wiki/src/views/tutorial/models.py b/docs/tutorials/wiki/src/views/tutorial/models.py
index 9761856c6..aa907aee5 100644
--- a/docs/tutorials/wiki/src/views/tutorial/models.py
+++ b/docs/tutorials/wiki/src/views/tutorial/models.py
@@ -10,7 +10,7 @@ class Page(Persistent):
self.data = data
def appmaker(zodb_root):
- if not 'app_root' in zodb_root:
+ if 'app_root' not in zodb_root:
app_root = Wiki()
frontpage = Page('This is the front page')
app_root['FrontPage'] = frontpage
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/theme.min.css b/docs/tutorials/wiki/src/views/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt
index 1b30f42b6..f8cbe2e2c 100644
--- a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt
@@ -34,14 +34,15 @@
<div class="col-md-10">
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
</div>
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
index e7b0dc23e..93580658b 100644
--- a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
@@ -8,7 +8,7 @@
<meta name="author" content="Pylons Project">
<link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
- <title>${page.name} - Pyramid tutorial wiki (based on
+ <title>${page.__name__} - Pyramid tutorial wiki (based on
TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
diff --git a/docs/tutorials/wiki/src/views/tutorial/tests.py b/docs/tutorials/wiki/src/views/tutorial/tests.py
index 663c9f405..40f3c47af 100644
--- a/docs/tutorials/wiki/src/views/tutorial/tests.py
+++ b/docs/tutorials/wiki/src/views/tutorial/tests.py
@@ -2,125 +2,16 @@ import unittest
from pyramid import testing
-class PageModelTests(unittest.TestCase):
- def _getTargetClass(self):
- from .models import Page
- return Page
+class ViewTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
- def _makeOne(self, data=u'some data'):
- return self._getTargetClass()(data=data)
+ def tearDown(self):
+ testing.tearDown()
- def test_constructor(self):
- instance = self._makeOne()
- self.assertEqual(instance.data, u'some data')
-
-class WikiModelTests(unittest.TestCase):
-
- def _getTargetClass(self):
- from .models import Wiki
- return Wiki
-
- def _makeOne(self):
- return self._getTargetClass()()
-
- def test_it(self):
- wiki = self._makeOne()
- self.assertEqual(wiki.__parent__, None)
- self.assertEqual(wiki.__name__, None)
-
-class AppmakerTests(unittest.TestCase):
-
- def _callFUT(self, zodb_root):
- from .models import appmaker
- return appmaker(zodb_root)
-
- def test_it(self):
- root = {}
- self._callFUT(root)
- self.assertEqual(root['app_root']['FrontPage'].data,
- 'This is the front page')
-
-class ViewWikiTests(unittest.TestCase):
- def test_it(self):
- from .views import view_wiki
- context = testing.DummyResource()
- request = testing.DummyRequest()
- response = view_wiki(context, request)
- self.assertEqual(response.location, 'http://example.com/FrontPage')
-
-class ViewPageTests(unittest.TestCase):
- def _callFUT(self, context, request):
- from .views import view_page
- return view_page(context, request)
-
- def test_it(self):
- wiki = testing.DummyResource()
- wiki['IDoExist'] = testing.DummyResource()
- context = testing.DummyResource(data='Hello CruelWorld IDoExist')
- context.__parent__ = wiki
- context.__name__ = 'thepage'
- request = testing.DummyRequest()
- info = self._callFUT(context, request)
- self.assertEqual(info['page'], context)
- self.assertEqual(
- info['content'],
- '<div class="document">\n'
- '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
- 'CruelWorld</a> '
- '<a href="http://example.com/IDoExist/">'
- 'IDoExist</a>'
- '</p>\n</div>\n')
- self.assertEqual(info['edit_url'],
- 'http://example.com/thepage/edit_page')
-
-
-class AddPageTests(unittest.TestCase):
- def _callFUT(self, context, request):
- from .views import add_page
- return add_page(context, request)
-
- def test_it_notsubmitted(self):
- context = testing.DummyResource()
+ def test_my_view(self):
+ from .views import my_view
request = testing.DummyRequest()
- request.subpath = ['AnotherPage']
- info = self._callFUT(context, request)
- self.assertEqual(info['page'].data,'')
- self.assertEqual(
- info['save_url'],
- request.resource_url(context, 'add_page', 'AnotherPage'))
-
- def test_it_submitted(self):
- context = testing.DummyResource()
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- request.subpath = ['AnotherPage']
- self._callFUT(context, request)
- page = context['AnotherPage']
- self.assertEqual(page.data, 'Hello yo!')
- self.assertEqual(page.__name__, 'AnotherPage')
- self.assertEqual(page.__parent__, context)
-
-class EditPageTests(unittest.TestCase):
- def _callFUT(self, context, request):
- from .views import edit_page
- return edit_page(context, request)
-
- def test_it_notsubmitted(self):
- context = testing.DummyResource()
- request = testing.DummyRequest()
- info = self._callFUT(context, request)
- self.assertEqual(info['page'], context)
- self.assertEqual(info['save_url'],
- request.resource_url(context, 'edit_page'))
-
- def test_it_submitted(self):
- context = testing.DummyResource()
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- response = self._callFUT(context, request)
- self.assertEqual(response.location, 'http://example.com/')
- self.assertEqual(context.data, 'Hello yo!')
-
-
-
+ info = my_view(request)
+ self.assertEqual(info['project'], 'tutorial')
diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst
index e255812fc..788ec595b 100644
--- a/docs/tutorials/wiki/tests.rst
+++ b/docs/tutorials/wiki/tests.rst
@@ -1,3 +1,5 @@
+.. _wiki_adding_tests:
+
============
Adding Tests
============
@@ -49,55 +51,25 @@ follows:
Running the tests
=================
-We can run these tests by using ``setup.py test`` in the same way we did in
-:ref:`running_tests`. However, first we must edit our ``setup.py`` to
-include a dependency on WebTest, which we've used in our ``tests.py``.
-Change the ``requires`` list in ``setup.py`` to include ``WebTest``.
-
-.. literalinclude:: src/tests/setup.py
- :linenos:
- :language: python
- :lines: 11-22
- :emphasize-lines: 11
-
-After we've added a dependency on WebTest in ``setup.py``, we need to run
-``setup.py develop`` to get WebTest installed into our virtualenv. Assuming
-our shell's current working directory is the "tutorial" distribution
-directory:
-
-On UNIX:
-
-.. code-block:: text
-
- $ $VENV/bin/python setup.py develop
-
-On Windows:
-
-.. code-block:: text
-
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
-
-Once that command has completed successfully, we can run the tests
-themselves:
+We can run these tests by using ``py.test`` similarly to how we did in
+:ref:`running_tests`. Our testing dependencies have already been satisfied,
+courtesy of the scaffold, so we can jump right to running tests.
On UNIX:
.. code-block:: text
- $ $VENV/bin/python setup.py test -q
+ $ $VENV/bin/py.test tutorial/tests.py -q
On Windows:
.. code-block:: text
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
+ c:\pyramidtut\tutorial> %VENV%\Scripts\py.test tutorial/tests.py -q
The expected result should look like the following:
.. code-block:: text
- .........
- ----------------------------------------------------------------------
- Ran 23 tests in 1.653s
-
- OK
+ ........................
+ 24 passed in 2.46 seconds
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
new file mode 100644
index 000000000..5447db861
--- /dev/null
+++ b/docs/tutorials/wiki2/authentication.rst
@@ -0,0 +1,312 @@
+.. _wiki2_adding_authentication:
+
+=====================
+Adding authentication
+=====================
+
+:app:`Pyramid` provides facilities for :term:`authentication` and
+:term:`authorization`. In this section we'll focus solely on the authentication
+APIs to add login and logout functionality to our wiki.
+
+We will implement authentication with the following steps:
+
+* Add an :term:`authentication policy` and a ``request.user`` computed property
+ (``security.py``).
+* Add routes for ``/login`` and ``/logout`` (``routes.py``).
+* Add login and logout views (``views/auth.py``).
+* Add a login template (``login.jinja2``).
+* Add "Login" and "Logout" links to every page based on the user's
+ authenticated state (``layout.jinja2``).
+* Make the existing views verify user state (``views/default.py``).
+* Redirect to ``/login`` when a user is denied access to any of the views that
+ require permission, instead of a default "403 Forbidden" page
+ (``views/auth.py``).
+
+
+Authenticating requests
+-----------------------
+
+The core of :app:`Pyramid` authentication is an :term:`authentication policy`
+which is used to identify authentication information from a ``request``,
+as well as handling the low-level login and logout operations required to
+track users across requests (via cookies, headers, or whatever else you can
+imagine).
+
+
+Add the authentication policy
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create a new file ``tutorial/security.py`` with the following content:
+
+.. literalinclude:: src/authentication/tutorial/security.py
+ :linenos:
+ :language: python
+
+Here we've defined:
+
+* A new authentication policy named ``MyAuthenticationPolicy``, which is
+ subclassed from Pyramid's
+ :class:`pyramid.authentication.AuthTktAuthenticationPolicy`, which tracks the
+ :term:`userid` using a signed cookie (lines 7-11).
+* A ``get_user`` function, which can convert the ``unauthenticated_userid``
+ from the policy into a ``User`` object from our database (lines 13-17).
+* The ``get_user`` is registered on the request as ``request.user`` to be used
+ throughout our application as the authenticated ``User`` object for the
+ logged-in user (line 27).
+
+The logic in this file is a little bit interesting, so we'll go into detail
+about what's happening here:
+
+First, the default authentication policies all provide a method named
+``unauthenticated_userid`` which is responsible for the low-level parsing
+of the information in the request (cookies, headers, etc.). If a ``userid``
+is found, then it is returned from this method. This is named
+``unauthenticated_userid`` because, at the lowest level, it knows the value of
+the userid in the cookie, but it doesn't know if it's actually a user in our
+system (remember, anything the user sends to our app is untrusted).
+
+Second, our application should only care about ``authenticated_userid`` and
+``request.user``, which have gone through our application-specific process of
+validating that the user is logged in.
+
+In order to provide an ``authenticated_userid`` we need a verification step.
+That can happen anywhere, so we've elected to do it inside of the cached
+``request.user`` computed property. This is a convenience that makes
+``request.user`` the source of truth in our system. It is either ``None`` or
+a ``User`` object from our database. This is why the ``get_user`` function
+uses the ``unauthenticated_userid`` to check the database.
+
+
+Configure the app
+~~~~~~~~~~~~~~~~~
+
+Since we've added a new ``tutorial/security.py`` module, we need to include it.
+Open the file ``tutorial/__init__.py`` and edit the following lines:
+
+.. literalinclude:: src/authentication/tutorial/__init__.py
+ :linenos:
+ :emphasize-lines: 11
+ :language: python
+
+Our authentication policy is expecting a new setting, ``auth.secret``. Open
+the file ``development.ini`` and add the highlighted line below:
+
+.. literalinclude:: src/authentication/development.ini
+ :lines: 18-20
+ :emphasize-lines: 3
+ :lineno-match:
+ :language: ini
+
+Finally, best practices tell us to use a different secret for production, so
+open ``production.ini`` and add a different secret:
+
+.. literalinclude:: src/authentication/production.ini
+ :lines: 15-17
+ :emphasize-lines: 3
+ :lineno-match:
+ :language: ini
+
+
+Add permission checks
+~~~~~~~~~~~~~~~~~~~~~
+
+:app:`Pyramid` has full support for declarative authorization, which we'll
+cover in the next chapter. However, many people looking to get their feet wet
+are just interested in authentication with some basic form of home-grown
+authorization. We'll show below how to accomplish the simple security goals of
+our wiki, now that we can track the logged-in state of users.
+
+Remember our goals:
+
+* Allow only ``editor`` and ``basic`` logged-in users to create new pages.
+* Only allow ``editor`` users and the page creator (possibly a ``basic`` user)
+ to edit pages.
+
+Open the file ``tutorial/views/default.py`` and fix the following imports:
+
+.. literalinclude:: src/authentication/tutorial/views/default.py
+ :lines: 5-13
+ :lineno-match:
+ :emphasize-lines: 2,9
+ :language: python
+
+Change the two highlighted lines.
+
+In the same file, now edit the ``edit_page`` view function:
+
+.. literalinclude:: src/authentication/tutorial/views/default.py
+ :lines: 45-60
+ :lineno-match:
+ :emphasize-lines: 5-7
+ :language: python
+
+Only the highlighted lines need to be changed.
+
+If the user either is not logged in or the user is not the page's creator
+*and* not an ``editor``, then we raise ``HTTPForbidden``.
+
+In the same file, now edit the ``add_page`` view function:
+
+.. literalinclude:: src/authentication/tutorial/views/default.py
+ :lines: 62-76
+ :lineno-match:
+ :emphasize-lines: 3-5,13
+ :language: python
+
+Only the highlighted lines need to be changed.
+
+If the user either is not logged in or is not in the ``basic`` or ``editor``
+roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden"
+response to the user. However, we will hook this later to redirect to the login
+page. Also, now that we have ``request.user``, we no longer have to hard-code
+the creator as the ``editor`` user, so we can finally drop that hack.
+
+These simple checks should protect our views.
+
+
+Login, logout
+-------------
+
+Now that we've got the ability to detect logged-in users, we need to add the
+``/login`` and ``/logout`` views so that they can actually login and logout!
+
+
+Add routes for ``/login`` and ``/logout``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Go back to ``tutorial/routes.py`` and add these two routes as highlighted:
+
+.. literalinclude:: src/authentication/tutorial/routes.py
+ :lines: 3-6
+ :lineno-match:
+ :emphasize-lines: 2-3
+ :language: python
+
+.. note:: The preceding lines must be added *before* the following
+ ``view_page`` route definition:
+
+ .. literalinclude:: src/authentication/tutorial/routes.py
+ :lines: 6
+ :lineno-match:
+ :language: python
+
+ This is because ``view_page``'s route definition uses a catch-all
+ "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`),
+ which will catch any route that was not already caught by any route
+ registered before it. Hence, for ``login`` and ``logout`` views to
+ have the opportunity of being matched (or "caught"), they must be above
+ ``/{pagename}``.
+
+
+Add login, logout, and forbidden views
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create a new file ``tutorial/views/auth.py``, and add the following code to it:
+
+.. literalinclude:: src/authentication/tutorial/views/auth.py
+ :linenos:
+ :language: python
+
+This code adds three new views to the application:
+
+- The ``login`` view renders a login form and processes the post from the
+ login form, checking credentials against our ``users`` table in the database.
+
+ The check is done by first finding a ``User`` record in the database, then
+ using our ``user.check_password`` method to compare the hashed passwords.
+
+ If the credentials are valid, then we use our authentication policy to store
+ the user's id in the response using :meth:`pyramid.security.remember`.
+
+ Finally, the user is redirected back to either the page which they were
+ trying to access (``next``) or the front page as a fallback. This parameter
+ is used by our forbidden view, as explained below, to finish the login
+ workflow.
+
+- The ``logout`` view handles requests to ``/logout`` by clearing the
+ credentials using :meth:`pyramid.security.forget`, then redirecting them to
+ the front page.
+
+- The ``forbidden_view`` is registered using the
+ :class:`pyramid.view.forbidden_view_config` decorator. This is a special
+ :term:`exception view`, which is invoked when a
+ :class:`pyramid.httpexceptions.HTTPForbidden` exception is raised.
+
+ This view will handle a forbidden error by redirecting the user to
+ ``/login``. As a convenience, it also sets the ``next=`` query string to the
+ current URL (the one that is forbidding access). This way, if the user
+ successfully logs in, they will be sent back to the page which they had been
+ trying to access.
+
+
+Add the ``login.jinja2`` template
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create ``tutorial/templates/login.jinja2`` with the following content:
+
+.. literalinclude:: src/authentication/tutorial/templates/login.jinja2
+ :language: html
+
+The above template is referenced in the login view that we just added in
+``tutorial/views/auth.py``.
+
+
+Add "Login" and "Logout" links
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Open ``tutorial/templates/layout.jinja2`` and add the following code as
+indicated by the highlighted lines.
+
+.. literalinclude:: src/authentication/tutorial/templates/layout.jinja2
+ :lines: 35-46
+ :lineno-match:
+ :emphasize-lines: 2-10
+ :language: html
+
+The ``request.user`` will be ``None`` if the user is not authenticated, or a
+``tutorial.models.User`` object if the user is authenticated. This check will
+make the logout link shown only when the user is logged in, and conversely the
+login link is only shown when the user is logged out.
+
+
+Viewing the application in a browser
+------------------------------------
+
+We can finally examine our application in a browser (See
+:ref:`wiki2-start-the-application`). Launch a browser and visit each of the
+following URLs, checking that the result is as expected:
+
+- http://localhost:6543/ invokes the ``view_wiki`` view. This always
+ redirects to the ``view_page`` view of the ``FrontPage`` page object. It
+ is executable by any user.
+
+- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
+ ``FrontPage`` page object. There is a "Login" link in the upper right corner
+ while the user is not authenticated, else it is a "Logout" link when the user
+ is authenticated.
+
+- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for
+ the ``FrontPage`` page object. It is executable by only the ``editor`` user.
+ If a different user (or the anonymous user) invokes it, then a login form
+ will be displayed. Supplying the credentials with the username ``editor`` and
+ password ``editor`` will display the edit page form.
+
+- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for
+ a page. If the page already exists, then it redirects the user to the
+ ``edit_page`` view for the page object. It is executable by either the
+ ``editor`` or ``basic`` user. If a different user (or the anonymous user)
+ invokes it, then a login form will be displayed. Supplying the credentials
+ with either the username ``editor`` and password ``editor``, or username
+ ``basic`` and password ``basic``, will display the edit page form.
+
+- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view
+ for an existing page, or generates an error if the page does not exist. It is
+ editable by the ``basic`` user if the page was created by that user in the
+ previous step. If, instead, the page was created by the ``editor`` user, then
+ the login page should be shown for the ``basic`` user.
+
+- After logging in (as a result of hitting an edit or add page and submitting
+ the login form with the ``editor`` credentials), we'll see a "Logout" link in
+ the upper right hand corner. When we click it, we're logged out, redirected
+ back to the front page, and a "Login" link is shown in the upper right hand
+ corner.
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index 1d810b05b..234f40e3b 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -4,375 +4,221 @@
Adding authorization
====================
-:app:`Pyramid` provides facilities for :term:`authentication` and
-:term:`authorization`. We'll make use of both features to provide security
-to our application. Our application currently allows anyone with access to
-the server to view, edit, and add pages to our wiki. We'll change that to
-allow only people who are members of a *group* named ``group:editors`` to add
-and edit wiki pages but we'll continue allowing anyone with access to the
-server to view pages.
-
-We will also add a login page and a logout link on all the pages. The login
-page will be shown when a user is denied access to any of the views that
-require permission, instead of a default "403 Forbidden" page.
-
-We will implement the access control with the following steps:
-
-* Add users and groups (``security.py``, a new module).
-* Add an :term:`ACL` (``models.py`` and ``__init__.py``).
-* Add an :term:`authentication policy` and an :term:`authorization policy`
- (``__init__.py``).
-* Add :term:`permission` declarations to the ``edit_page`` and ``add_page``
- views (``views.py``).
-
-Then we will add the login and logout feature:
-
-* Add routes for /login and /logout (``__init__.py``).
-* Add ``login`` and ``logout`` views (``views.py``).
-* Add a login template (``login.pt``).
-* Make the existing views return a ``logged_in`` flag to the renderer
- (``views.py``).
-* Add a "Logout" link to be shown when logged in and viewing or editing a page
- (``view.pt``, ``edit.pt``).
-
-
-Access control
---------------
-
-Add users and groups
-~~~~~~~~~~~~~~~~~~~~
-
-Create a new ``tutorial/tutorial/security.py`` module with the
-following content:
+In the last chapter we built :term:`authentication` into our wiki. We also
+went one step further and used the ``request.user`` object to perform some
+explicit :term:`authorization` checks. This is fine for a lot of applications,
+but :app:`Pyramid` provides some facilities for cleaning this up and decoupling
+the constraints from the view function itself.
+
+We will implement access control with the following steps:
+
+* Update the :term:`authentication policy` to break down the :term:`userid`
+ into a list of :term:`principals <principal>` (``security.py``).
+* Define an :term:`authorization policy` for mapping users, resources and
+ permissions (``security.py``).
+* Add new :term:`resource` definitions that will be used as the :term:`context`
+ for the wiki pages (``routes.py``).
+* Add an :term:`ACL` to each resource (``routes.py``).
+* Replace the inline checks on the views with :term:`permission` declarations
+ (``views/default.py``).
+
+
+Add user principals
+-------------------
+
+A :term:`principal` is a level of abstraction on top of the raw :term:`userid`
+that describes the user in terms of its capabilities, roles, or other
+identifiers that are easier to generalize. The permissions are then written
+against the principals without focusing on the exact user involved.
+
+:app:`Pyramid` defines two builtin principals used in every application:
+:attr:`pyramid.security.Everyone` and :attr:`pyramid.security.Authenticated`.
+On top of these we have already mentioned the required principals for this
+application in the original design. The user has two possible roles: ``editor``
+or ``basic``. These will be prefixed by the string ``role:`` to avoid clashing
+with any other types of principals.
+
+Open the file ``tutorial/security.py`` and edit it as follows:
.. literalinclude:: src/authorization/tutorial/security.py
:linenos:
+ :emphasize-lines: 3-6,17-24
:language: python
-The ``groupfinder`` function accepts a userid and a request and
-returns one of these values:
+Only the highlighted lines need to be added.
-- If the userid exists in the system, it will return a sequence of group
- identifiers (or an empty sequence if the user isn't a member of any groups).
-- If the userid *does not* exist in the system, it will return ``None``.
+Note that the role comes from the ``User`` object. We also add the ``user.id``
+as a principal for when we want to allow that exact user to edit pages which
+they have created.
-For example, ``groupfinder('editor', request )`` returns ``['group:editor']``,
-``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin',
-request)`` returns ``None``. We will use ``groupfinder()`` as an
-:term:`authentication policy` "callback" that will provide the
-:term:`principal` or principals for a user.
-In a production system, user and group data will most often come from a
-database, but here we use "dummy" data to represent user and groups sources.
+Add the authorization policy
+----------------------------
-Add an ACL
-~~~~~~~~~~
+We already added the :term:`authorization policy` in the previous chapter
+because :app:`Pyramid` requires one when adding an
+:term:`authentication policy`. However, it was not used anywhere, so we'll
+mention it now.
-Open ``tutorial/tutorial/models.py`` and add the following import
-statement at the head:
+In the file ``tutorial/security.py``, notice the following lines:
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 1-4
- :linenos:
+.. literalinclude:: src/authorization/tutorial/security.py
+ :lines: 38-40
+ :lineno-match:
+ :emphasize-lines: 2
:language: python
-Add the following class definition at the end:
+We're using the :class:`pyramid.authorization.ACLAuthorizationPolicy`, which
+will suffice for most applications. It uses the :term:`context` to define the
+mapping between a :term:`principal` and :term:`permission` for the current
+request via the ``__acl__``.
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 33-37
- :linenos:
- :lineno-start: 33
- :language: python
-We import :data:`~pyramid.security.Allow`, an action that means that
-permission is allowed, and :data:`~pyramid.security.Everyone`, a special
-:term:`principal` that is associated to all requests. Both are used in the
-:term:`ACE` entries that make up the ACL.
+Add resources and ACLs
+----------------------
-The ACL is a list that needs to be named `__acl__` and be an attribute of a
-class. We define an :term:`ACL` with two :term:`ACE` entries: the first entry
-allows any user the `view` permission. The second entry allows the
-``group:editors`` principal the `edit` permission.
+Resources are the hidden gem of :app:`Pyramid`. You've made it!
-The ``RootFactory`` class that contains the ACL is a :term:`root factory`. We
-need to associate it to our :app:`Pyramid` application, so the ACL is provided
-to each view in the :term:`context` of the request as the ``context``
-attribute.
+Every URL in a web application represents a :term:`resource` (the "R" in
+Uniform Resource Locator). Often the resource is something in your data model,
+but it could also be an abstraction over many models.
-Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to
-our :term:`Configurator` constructor, that points to the class we created
-above:
+Our wiki has two resources:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 24-25
- :linenos:
- :emphasize-lines: 2
- :lineno-start: 16
- :language: python
+#. A ``NewPage``. Represents a potential ``Page`` that does not exist. Any
+ logged-in user, having either role of ``basic`` or ``editor``, can create
+ pages.
-Only the highlighted line needs to be added.
+#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited.
+ ``editor`` users, as well as the original creator of the ``Page``, may edit
+ the ``PageResource``. Anyone may view it.
-We are now providing the ACL to the application. See :ref:`assigning_acls`
-for more information about what an :term:`ACL` represents.
+.. note::
-.. note:: Although we don't use the functionality here, the ``factory`` used
- to create route contexts may differ per-route as opposed to globally. See
- the ``factory`` argument to :meth:`pyramid.config.Configurator.add_route`
- for more info.
+ The wiki data model is simple enough that the ``PageResource`` is mostly
+ redundant with our ``models.Page`` SQLAlchemy class. It is completely valid
+ to combine these into one class. However, for this tutorial, they are
+ explicitly separated to make clear the distinction between the parts about
+ which :app:`Pyramid` cares versus application-defined objects.
-Add authentication and authorization policies
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+There are many ways to define these resources, and they can even be grouped
+into collections with a hierarchy. However, we're keeping it simple here!
-Open ``tutorial/tutorial/__init__.py`` and add the highlighted import
-statements:
+Open the file ``tutorial/routes.py`` and edit the following lines:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 1-7
+.. literalinclude:: src/authorization/tutorial/routes.py
:linenos:
- :emphasize-lines: 2-3,7
+ :emphasize-lines: 1-11,17-
:language: python
-Now add those policies to the configuration:
+The highlighted lines need to be edited or added.
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 21-27
- :linenos:
- :lineno-start: 21
- :emphasize-lines: 1-3,6-7
- :language: python
+The ``NewPage`` class has an ``__acl__`` on it that returns a list of mappings
+from :term:`principal` to :term:`permission`. This defines *who* can do *what*
+with that :term:`resource`. In our case we want to allow only those users with
+the principals of either ``role:editor`` or ``role:basic`` to have the
+``create`` permission:
-Only the highlighted lines need to be added.
-
-We are enabling an ``AuthTktAuthenticationPolicy``, which is based in an auth
-ticket that may be included in the request. We are also enabling an
-``ACLAuthorizationPolicy``, which uses an ACL to determine the *allow* or
-*deny* outcome for a view.
-
-Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy`
-constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is
-a string representing an encryption key used by the "authentication ticket"
-machinery represented by this policy: it is required. The ``callback`` is the
-``groupfinder()`` function that we created before.
-
-Add permission declarations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/tutorial/views.py`` and add a ``permission='edit'`` parameter
-to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 60-61
- :emphasize-lines: 1-2
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 30-38
+ :lineno-match:
+ :emphasize-lines: 5-9
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 75-76
- :emphasize-lines: 1-2
- :language: python
-
-Only the highlighted lines, along with their preceding commas, need to be
-edited and added.
-
-The result is that only users who possess the ``edit`` permission at the time
-of the request may invoke those two views.
+The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by
+declaring a ``factory`` on the route:
-Add a ``permission='view'`` parameter to the ``@view_config`` decorator for
-``view_wiki()`` and ``view_page()`` as follows:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 30-31
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 18-19
+ :lineno-match:
:emphasize-lines: 1-2
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 36-37
- :emphasize-lines: 1-2
- :language: python
-
-Only the highlighted lines, along with their preceding commas, need to be
-edited and added.
-
-This allows anyone to invoke these two views.
-
-We are done with the changes needed to control access. The changes that
-follow will add the login and logout feature.
-
-Login, logout
--------------
-
-Add routes for /login and /logout
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Go back to ``tutorial/tutorial/__init__.py`` and add these two routes as
-highlighted:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 30-33
- :emphasize-lines: 2-3
- :language: python
-
-.. note:: The preceding lines must be added *before* the following
- ``view_page`` route definition:
-
- .. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 33
- :language: python
-
- This is because ``view_page``'s route definition uses a catch-all
- "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`)
- which will catch any route that was not already caught by any route listed
- above it in ``__init__.py``. Hence, for ``login`` and ``logout`` views to
- have the opportunity of being matched (or "caught"), they must be above
- ``/{pagename}``.
-
-Add login and logout views
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an
+actual ``Page`` object to determine *who* can do *what* to the page.
-We'll add a ``login`` view which renders a login form and processes the post
-from the login form, checking credentials.
-
-We'll also add a ``logout`` view callable to our application and provide a
-link to it. This view will clear the credentials of the logged in user and
-redirect back to the front page.
-
-Add the following import statements to the head of
-``tutorial/tutorial/views.py``:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 9-19
- :emphasize-lines: 1-11
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 47-
+ :lineno-match:
+ :emphasize-lines: 5-10
:language: python
-All the highlighted lines need to be added or edited.
-
-:meth:`~pyramid.view.forbidden_view_config` will be used to customize the
-default 403 Forbidden page. :meth:`~pyramid.security.remember` and
-:meth:`~pyramid.security.forget` help to create and expire an auth ticket
-cookie.
-
-Now add the ``login`` and ``logout`` views at the end of the file:
+The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and
+``edit_page`` routes by declaring a ``factory`` on the routes:
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 91-123
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 17-21
+ :lineno-match:
+ :emphasize-lines: 1,4-5
:language: python
-``login()`` has two decorators:
-- a ``@view_config`` decorator which associates it with the ``login`` route
- and makes it visible when we visit ``/login``,
-- a ``@forbidden_view_config`` decorator which turns it into a
- :term:`forbidden view`. ``login()`` will be invoked when a user tries to
- execute a view callable for which they lack authorization. For example, if
- a user has not logged in and tries to add or edit a Wiki page, they will be
- shown the login form before being allowed to continue.
+Add view permissions
+--------------------
-The order of these two :term:`view configuration` decorators is unimportant.
+At this point we've modified our application to load the ``PageResource``,
+including the actual ``Page`` model in the ``page_factory``. The
+``PageResource`` is now the :term:`context` for all ``view_page`` and
+``edit_page`` views. Similarly the ``NewPage`` will be the context for the
+``add_page`` view.
-``logout()`` is decorated with a ``@view_config`` decorator which associates
-it with the ``logout`` route. It will be invoked when we visit ``/logout``.
+Open the file ``tutorial/views/default.py``.
-Add the ``login.pt`` Template
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+First, you can drop a few imports that are no longer necessary:
-Create ``tutorial/tutorial/templates/login.pt`` with the following content:
-
-.. literalinclude:: src/authorization/tutorial/templates/login.pt
- :language: html
-
-The above template is referenced in the login view that we just added in
-``views.py``.
-
-Return a ``logged_in`` flag to the renderer
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Open ``tutorial/tutorial/views.py`` again. Add a ``logged_in`` parameter to
-the return value of ``view_page()``, ``edit_page()``, and ``add_page()`` as
-follows:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 57-58
- :emphasize-lines: 1-2
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 5-7
+ :lineno-match:
+ :emphasize-lines: 1
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 72-73
- :emphasize-lines: 1-2
- :language: python
+Edit the ``view_page`` view to declare the ``view`` permission, and remove the
+explicit checks within the view:
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 85-89
- :emphasize-lines: 3-4
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 18-23
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
-
-The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if
-the user is not authenticated, or a userid if the user is authenticated.
-
-Add a "Logout" link when logged in
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The work of loading the page has already been done in the factory, so we can
+just pull the ``page`` object out of the ``PageResource``, loaded as
+``request.context``. Our factory also guarantees we will have a ``Page``, as it
+raises the ``HTTPNotFound`` exception if no ``Page`` exists, again simplifying
+the view logic.
-Open ``tutorial/tutorial/templates/edit.pt`` and
-``tutorial/tutorial/templates/view.pt`` and add the following code as
-indicated by the highlighted lines.
+Edit the ``edit_page`` view to declare the ``edit`` permission:
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :lines: 34-38
- :emphasize-lines: 3-5
- :language: html
-
-The attribute ``tal:condition="logged_in"`` will make the element be included
-when ``logged_in`` is any user id. The link will invoke the logout view. The
-above element will not be included if ``logged_in`` is ``None``, such as when
-a user is not authenticated.
-
-Reviewing our changes
----------------------
-
-Our ``tutorial/tutorial/__init__.py`` will look like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :linenos:
- :emphasize-lines: 2-3,7,21-23,25-27,31-32
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 38-42
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
+Edit the ``add_page`` view to declare the ``create`` permission:
-Our ``tutorial/tutorial/models.py`` will look like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/models.py
- :linenos:
- :emphasize-lines: 1-4,33-37
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 52-56
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/views.py`` will look like this when we're done:
+Note the ``pagename`` here is pulled off of the context instead of
+``request.matchdict``. The factory has done a lot of work for us to hide the
+actual route pattern.
-.. literalinclude:: src/authorization/tutorial/views.py
- :linenos:
- :emphasize-lines: 9-11,14-19,25,31,37,58,61,73,76,88,91-117,119-123
- :language: python
+The ACLs defined on each :term:`resource` are used by the :term:`authorization
+policy` to determine if any :term:`principal` is allowed to have some
+:term:`permission`. If this check fails (for example, the user is not logged
+in) then an ``HTTPForbidden`` exception will be raised automatically. Thus
+we're able to drop those exceptions and checks from the views themselves.
+Rather we've defined them in terms of operations on a resource.
-Only the highlighted lines need to be added or edited.
+The final ``tutorial/views/default.py`` should look like the following:
-Our ``tutorial/tutorial/templates/edit.pt`` template will look like this when
-we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :linenos:
- :emphasize-lines: 36-38
- :language: html
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/templates/view.pt`` template will look like this when
-we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/view.pt
+.. literalinclude:: src/authorization/tutorial/views/default.py
:linenos:
- :emphasize-lines: 36-38
- :language: html
-
-Only the highlighted lines need to be added or edited.
+ :language: python
Viewing the application in a browser
------------------------------------
@@ -386,21 +232,32 @@ following URLs, checking that the result is as expected:
is executable by any user.
- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
- ``FrontPage`` page object.
-
-- http://localhost:6543/FrontPage/edit_page invokes the edit view for the
- FrontPage object. It is executable by only the ``editor`` user. If a
- different user (or the anonymous user) invokes it, a login form will be
- displayed. Supplying the credentials with the username ``editor``, password
- ``editor`` will display the edit page form.
-
-- http://localhost:6543/add_page/SomePageName invokes the add view for a page.
- It is executable by only the ``editor`` user. If a different user (or the
- anonymous user) invokes it, a login form will be displayed. Supplying the
- credentials with the username ``editor``, password ``editor`` will display
- the edit page form.
+ ``FrontPage`` page object. There is a "Login" link in the upper right corner
+ while the user is not authenticated, else it is a "Logout" link when the user
+ is authenticated.
+
+- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for
+ the ``FrontPage`` page object. It is executable by only the ``editor`` user.
+ If a different user (or the anonymous user) invokes it, then a login form
+ will be displayed. Supplying the credentials with the username ``editor`` and
+ password ``editor`` will display the edit page form.
+
+- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for
+ a page. If the page already exists, then it redirects the user to the
+ ``edit_page`` view for the page object. It is executable by either the
+ ``editor`` or ``basic`` user. If a different user (or the anonymous user)
+ invokes it, then a login form will be displayed. Supplying the credentials
+ with either the username ``editor`` and password ``editor``, or username
+ ``basic`` and password ``basic``, will display the edit page form.
+
+- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view
+ for an existing page, or generates an error if the page does not exist. It is
+ editable by the ``basic`` user if the page was created by that user in the
+ previous step. If, instead, the page was created by the ``editor`` user, then
+ the login page should be shown for the ``basic`` user.
- After logging in (as a result of hitting an edit or add page and submitting
- the login form with the ``editor`` credentials), we'll see a Logout link in
- the upper right hand corner. When we click it, we're logged out, and
- redirected back to the front page.
+ the login form with the ``editor`` credentials), we'll see a "Logout" link in
+ the upper right hand corner. When we click it, we're logged out, redirected
+ back to the front page, and a "Login" link is shown in the upper right hand
+ corner.
diff --git a/docs/tutorials/wiki2/background.rst b/docs/tutorials/wiki2/background.rst
index b8afb8305..ee7dfe36f 100644
--- a/docs/tutorials/wiki2/background.rst
+++ b/docs/tutorials/wiki2/background.rst
@@ -1,3 +1,5 @@
+.. _wiki2_background:
+
==========
Background
==========
@@ -5,13 +7,13 @@ Background
This version of the :app:`Pyramid` wiki tutorial presents a
:app:`Pyramid` application that uses technologies which will be
familiar to someone with SQL database experience. It uses
-:term:`SQLAlchemy` as a persistence mechanism and :term:`url dispatch` to map
+:term:`SQLAlchemy` as a persistence mechanism and :term:`URL dispatch` to map
URLs to code. It can also be followed by people without any prior
Python web framework experience.
To code along with this tutorial, the developer will need a UNIX
machine with development tools (Mac OS X with XCode, any Linux or BSD
-variant, etc) *or* a Windows system of any kind.
+variant, etc.) *or* a Windows system of any kind.
.. note::
diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst
index 695d7f15b..ce67bb9e3 100644
--- a/docs/tutorials/wiki2/basiclayout.rst
+++ b/docs/tutorials/wiki2/basiclayout.rst
@@ -1,3 +1,5 @@
+.. _wiki2_basic_layout:
+
============
Basic Layout
============
@@ -12,230 +14,237 @@ Application configuration with ``__init__.py``
A directory on disk can be turned into a Python :term:`package` by containing
an ``__init__.py`` file. Even if empty, this marks a directory as a Python
-package. We use ``__init__.py`` both as a marker, indicating the directory
-in which it's contained is a package, and to contain application configuration
+package. We use ``__init__.py`` both as a marker, indicating the directory in
+which it's contained is a package, and to contain application configuration
code.
-Open ``tutorial/tutorial/__init__.py``. It should already contain
-the following:
+Open ``tutorial/__init__.py``. It should already contain the following:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :linenos:
+ :language: py
-Let's go over this piece-by-piece. First, we need some imports to support
-later code:
+Let's go over this piece-by-piece. First we need some imports to support later
+code:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :end-before: main
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :end-before: main
+ :linenos:
+ :lineno-match:
+ :language: py
``__init__.py`` defines a function named ``main``. Here is the entirety of
the ``main`` function we've defined in our ``__init__.py``:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :pyobject: main
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :pyobject: main
+ :linenos:
+ :lineno-match:
+ :language: py
When you invoke the ``pserve development.ini`` command, the ``main`` function
above is executed. It accepts some settings and returns a :term:`WSGI`
application. (See :ref:`startup_chapter` for more about ``pserve``.)
-The main function first creates a :term:`SQLAlchemy` database engine using
-:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.`` prefixed
-settings in the ``development.ini`` file's ``[app:main]`` section.
-This will be a URI (something like ``sqlite://``):
+Next in ``main``, construct a :term:`Configurator` object:
+
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 7
+ :lineno-match:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 13
- :language: py
+``settings`` is passed to the ``Configurator`` as a keyword argument with the
+dictionary values passed as the ``**settings`` argument. This will be a
+dictionary of settings parsed from the ``.ini`` file, which contains
+deployment-related values, such as ``pyramid.reload_templates``,
+``sqlalchemy.url``, and so on.
-``main`` then initializes our SQLAlchemy session object, passing it the
-engine:
+Next include :term:`Jinja2` templating bindings so that we can use renderers
+with the ``.jinja2`` extension within our project.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 14
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 8
+ :lineno-match:
+ :language: py
-``main`` subsequently initializes our SQLAlchemy declarative ``Base`` object,
-assigning the engine we created to the ``bind`` attribute of it's
-``metadata`` object. This allows table definitions done imperatively
-(instead of declaratively, via a class statement) to work. We won't use any
-such tables in our application, but if you add one later, long after you've
-forgotten about this tutorial, you won't be left scratching your head when it
-doesn't work.
+Next include the the package ``models`` using a dotted Python path. The exact
+setup of the models will be covered later.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 15
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 9
+ :lineno-match:
+ :language: py
-The next step of ``main`` is to construct a :term:`Configurator` object:
+Next include the ``routes`` module using a dotted Python path. This module will
+be explained in the next section.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 16
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 10
+ :lineno-match:
+ :language: py
+
+.. note::
+
+ Pyramid's :meth:`pyramid.config.Configurator.include` method is the primary
+ mechanism for extending the configurator and breaking your code into
+ feature-focused modules.
+
+``main`` next calls the ``scan`` method of the configurator
+(:meth:`pyramid.config.Configurator.scan`), which will recursively scan our
+``tutorial`` package, looking for ``@view_config`` and other special
+decorators. When it finds a ``@view_config`` decorator, a view configuration
+will be registered, allowing one of our application URLs to be mapped to some
+code.
+
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 11
+ :lineno-match:
+ :language: py
+
+Finally ``main`` is finished configuring things, so it uses the
+:meth:`pyramid.config.Configurator.make_wsgi_app` method to return a
+:term:`WSGI` application:
+
+.. literalinclude:: src/basiclayout/tutorial/__init__.py
+ :lines: 12
+ :lineno-match:
+ :language: py
-``settings`` is passed to the Configurator as a keyword argument with the
-dictionary values passed as the ``**settings`` argument. This will be a
-dictionary of settings parsed from the ``.ini`` file, which contains
-deployment-related values such as ``pyramid.reload_templates``,
-``db_string``, etc.
-Next, include :term:`Chameleon` templating bindings so that we can use
-renderers with the ``.pt`` extension within our project.
+Route declarations
+------------------
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 17
- :language: py
+Open the ``tutorials/routes.py`` file. It should already contain the following:
-``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with
-two arguments: ``static`` (the name), and ``static`` (the path):
+.. literalinclude:: src/basiclayout/tutorial/routes.py
+ :linenos:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 18
- :language: py
+On line 2, we call :meth:`pyramid.config.Configurator.add_static_view` with
+three arguments: ``static`` (the name), ``static`` (the path), and
+``cache_max_age`` (a keyword argument).
This registers a static resource view which will match any URL that starts
with the prefix ``/static`` (by virtue of the first argument to
-``add_static_view``). This will serve up static resources for us from within
-the ``static`` directory of our ``tutorial`` package, in this case, via
+``add_static_view``). This will serve up static resources for us from within
+the ``static`` directory of our ``tutorial`` package, in this case via
``http://localhost:6543/static/`` and below (by virtue of the second argument
to ``add_static_view``). With this declaration, we're saying that any URL that
starts with ``/static`` should go to the static view; any remainder of its
-path (e.g. the ``/foo`` in ``/static/foo``) will be used to compose a path to
+path (e.g., the ``/foo`` in ``/static/foo``) will be used to compose a path to
a static file resource, such as a CSS file.
-Using the configurator ``main`` also registers a :term:`route configuration`
-via the :meth:`pyramid.config.Configurator.add_route` method that will be
-used when the URL is ``/``:
+On line 3, the module registers a :term:`route configuration` via the
+:meth:`pyramid.config.Configurator.add_route` method that will be used when the
+URL is ``/``. Since this route has a ``pattern`` equaling ``/``, it is the
+route that will be matched when the URL ``/`` is visited, e.g.,
+``http://localhost:6543/``.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 19
- :language: py
-Since this route has a ``pattern`` equaling ``/`` it is the route that will
-be matched when the URL ``/`` is visited, e.g. ``http://localhost:6543/``.
-
-``main`` next calls the ``scan`` method of the configurator
-(:meth:`pyramid.config.Configurator.scan`), which will recursively scan our
-``tutorial`` package, looking for ``@view_config`` (and
-other special) decorators. When it finds a ``@view_config`` decorator, a
-view configuration will be registered, which will allow one of our
-application URLs to be mapped to some code.
-
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 20
- :language: py
-
-Finally, ``main`` is finished configuring things, so it uses the
-:meth:`pyramid.config.Configurator.make_wsgi_app` method to return a
-:term:`WSGI` application:
-
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 21
- :language: py
-
-View declarations via ``views.py``
-----------------------------------
+View declarations via the ``views`` package
+-------------------------------------------
The main function of a web framework is mapping each URL pattern to code (a
:term:`view callable`) that is executed when the requested URL matches the
corresponding :term:`route`. Our application uses the
:meth:`pyramid.view.view_config` decorator to perform this mapping.
-Open ``tutorial/tutorial/views.py``. It should already contain the following:
+Open ``tutorial/views/default.py`` in the ``views`` package. It should already
+contain the following:
- .. literalinclude:: src/basiclayout/tutorial/views.py
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/views/default.py
+ :linenos:
+ :language: py
The important part here is that the ``@view_config`` decorator associates the
-function it decorates (``my_view``) with a :term:`view configuration`,
+function it decorates (``my_view``) with a :term:`view configuration`,
consisting of:
* a ``route_name`` (``home``)
- * a ``renderer``, which is a template from the ``templates`` subdirectory
- of the package.
+ * a ``renderer``, which is a template from the ``templates`` subdirectory of
+ the package.
When the pattern associated with the ``home`` view is matched during a request,
-``my_view()`` will be executed. ``my_view()`` returns a dictionary; the
-renderer will use the ``templates/mytemplate.pt`` template to create a response
-based on the values in the dictionary.
+``my_view()`` will be executed. ``my_view()`` returns a dictionary; the
+renderer will use the ``templates/mytemplate.jinja2`` template to create a
+response based on the values in the dictionary.
Note that ``my_view()`` accepts a single argument named ``request``. This is
the standard call signature for a Pyramid :term:`view callable`.
Remember in our ``__init__.py`` when we executed the
-:meth:`pyramid.config.Configurator.scan` method ``config.scan()``? The
-purpose of calling the scan method was to find and process this
-``@view_config`` decorator in order to create a view configuration within our
-application. Without being processed by ``scan``, the decorator effectively
-does nothing. ``@view_config`` is inert without being detected via a
-:term:`scan`.
-
-The sample ``my_view()`` created by the scaffold uses a ``try:`` and ``except:``
-clause to detect if there is a problem accessing the project database and
-provide an alternate error response. That response will include the text
-shown at the end of the file, which will be displayed in the browser to
-inform the user about possible actions to take to solve the problem.
-
-Content Models with ``models.py``
----------------------------------
-
-In a SQLAlchemy-based application, a *model* object is an object composed by
-querying the SQL database. The ``models.py`` file is where the ``alchemy``
-scaffold put the classes that implement our models.
+:meth:`pyramid.config.Configurator.scan` method ``config.scan()``? The purpose
+of calling the scan method was to find and process this ``@view_config``
+decorator in order to create a view configuration within our application.
+Without being processed by ``scan``, the decorator effectively does nothing.
+``@view_config`` is inert without being detected via a :term:`scan`.
-Open ``tutorial/tutorial/models.py``. It should already contain the following:
+The sample ``my_view()`` created by the scaffold uses a ``try:`` and
+``except:`` clause to detect if there is a problem accessing the project
+database and provide an alternate error response. That response will include
+the text shown at the end of the file, which will be displayed in the browser
+to inform the user about possible actions to take to solve the problem.
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :linenos:
- :language: py
-Let's examine this in detail. First, we need some imports to support later code:
+Content models with the ``models`` package
+------------------------------------------
+
+In an SQLAlchemy-based application, a *model* object is an object composed by
+querying the SQL database. The ``models`` package is where the ``alchemy``
+scaffold put the classes that implement our models.
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :end-before: DBSession
- :linenos:
- :language: py
+First, open ``tutorial/models/meta.py``, which should already contain the
+following:
-Next we set up a SQLAlchemy ``DBSession`` object:
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :linenos:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :lines: 17
- :language: py
+``meta.py`` contains imports and support code for defining the models. We
+create a dictionary ``NAMING_CONVENTION`` as well for consistent naming of
+support objects like indices and constraints.
-``scoped_session`` and ``sessionmaker`` are standard SQLAlchemy helpers.
-``scoped_session`` allows us to access our database connection globally.
-``sessionmaker`` creates a database session object. We pass to
-``sessionmaker`` the ``extension=ZopeTransactionExtension()`` extension
-option in order to allow the system to automatically manage database
-transactions. With ``ZopeTransactionExtension`` activated, our application
-will automatically issue a transaction commit after every request unless an
-exception is raised, in which case the transaction will be aborted.
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :end-before: metadata
+ :linenos:
+ :language: py
-We also need to create a declarative ``Base`` object to use as a
-base class for our model:
+Next we create a ``metadata`` object from the class
+:class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value
+for the ``naming_convention`` argument.
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :lines: 18
- :language: py
+A ``MetaData`` object represents the table and other schema definitions for a
+single database. We also need to create a declarative ``Base`` object to use as
+a base class for our models. Our models will inherit from this ``Base``, which
+will attach the tables to the ``metadata`` we created, and define our
+application's database schema.
-Our model classes will inherit from this ``Base`` class so they can be
-associated with our particular database connection.
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :lines: 15-16
+ :lineno-match:
+ :linenos:
+ :language: py
-To give a simple example of a model class, we define one named ``MyModel``:
+Next open ``tutorial/models/mymodel.py``, which should already contain the
+following:
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :pyobject: MyModel
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
+ :linenos:
+ :language: py
+
+Notice we've defined the ``models`` as a package to make it straightforward for
+defining models in separate modules. To give a simple example of a model class,
+we have defined one named ``MyModel`` in ``mymodel.py``:
+
+.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
+ :pyobject: MyModel
+ :lineno-match:
+ :linenos:
+ :language: py
Our example model does not require an ``__init__`` method because SQLAlchemy
-supplies for us a default constructor if one is not already present,
-which accepts keyword arguments of the same name as that of the mapped attributes.
+supplies for us a default constructor, if one is not already present, which
+accepts keyword arguments of the same name as that of the mapped attributes.
.. note:: Example usage of MyModel:
@@ -247,8 +256,83 @@ The ``MyModel`` class has a ``__tablename__`` attribute. This informs
SQLAlchemy which table to use to store the data representing instances of this
class.
-The Index import and the Index object creation is not required for this
-tutorial, and will be removed in the next step.
+Finally, open ``tutorial/models/__init__.py``, which should already
+contain the following:
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :linenos:
+ :language: py
+
+Our ``models/__init__.py`` module defines the primary API we will use for
+configuring the database connections within our application, and it contains
+several functions we will cover below.
+
+As we mentioned above, the purpose of the ``models.meta.metadata`` object is to
+describe the schema of the database. This is done by defining models that
+inherit from the ``Base`` object attached to that ``metadata`` object. In
+Python, code is only executed if it is imported, and so to attach the
+``models`` table defined in ``mymodel.py`` to the ``metadata``, we must import
+it. If we skip this step, then later, when we run
+:meth:`sqlalchemy.schema.MetaData.create_all`, the table will not be created
+because the ``metadata`` object does not know about it!
+
+Another important reason to import all of the models is that, when defining
+relationships between models, they must all exist in order for SQLAlchemy to
+find and build those internal mappings. This is why, after importing all the
+models, we explicitly execute the function
+:func:`sqlalchemy.orm.configure_mappers`, once we are sure all the models have
+been defined and before we start creating connections.
+
+Next we define several functions for connecting to our database. The first and
+lowest level is the ``get_engine`` function. This creates an :term:`SQLAlchemy`
+database engine using :func:`sqlalchemy.engine_from_config` from the
+``sqlalchemy.``-prefixed settings in the ``development.ini`` file's
+``[app:main]`` section. This setting is a URI (something like ``sqlite://``).
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :pyobject: get_engine
+ :lineno-match:
+ :linenos:
+ :language: py
+
+The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database
+engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` class
+:class:`sqlalchemy.orm.session.sessionmaker`. This ``session_factory`` is then
+used for creating sessions bound to the database engine.
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :pyobject: get_session_factory
+ :lineno-match:
+ :linenos:
+ :language: py
+
+The function ``get_tm_session`` registers a database session with a transaction
+manager, and returns a ``dbsession`` object. With the transaction manager, our
+application will automatically issue a transaction commit after every request,
+unless an exception is raised, in which case the transaction will be aborted.
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :pyobject: get_tm_session
+ :lineno-match:
+ :linenos:
+ :language: py
+
+Finally, we define an ``includeme`` function, which is a hook for use with
+:meth:`pyramid.config.Configurator.include` to activate code in a Pyramid
+application add-on. It is the code that is executed above when we ran
+``config.include('.models')`` in our application's ``main`` function. This
+function will take the settings from the application, create an engine, and
+define a ``request.dbsession`` property, which we can use to do work on behalf
+of an incoming request to our application.
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :pyobject: includeme
+ :lineno-match:
+ :linenos:
+ :language: py
That's about all there is to it regarding models, views, and initialization
code in our stock application.
+
+The ``Index`` import and the ``Index`` object creation in ``mymodel.py`` is
+not required for this tutorial, and will be removed in the next step.
diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst
index b2d9bf83a..6520613ea 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -1,127 +1,263 @@
+.. _wiki2_defining_the_domain_model:
+
=========================
Defining the Domain Model
=========================
The first change we'll make to our stock ``pcreate``-generated application will
-be to define a :term:`domain model` constructor representing a wiki page.
-We'll do this inside our ``models.py`` file.
+be to define a wiki page :term:`domain model`.
+.. note::
-Edit ``models.py``
-------------------
+ There is nothing special about the filename ``user.py`` or ``page.py`` except
+ that they are Python modules. A project may have many models throughout its
+ codebase in arbitrarily named modules. Modules implementing models often
+ have ``model`` in their names or they may live in a Python subpackage of
+ your application package named ``models`` (as we've done in this tutorial),
+ but this is only a convention and not a requirement.
-.. note::
- There is nothing special about the filename ``models.py``. A
- project may have many models throughout its codebase in arbitrarily named
- files. Files implementing models often have ``model`` in their filenames
- or they may live in a Python subpackage of your application package named
- ``models``, but this is only by convention.
+Declaring dependencies in our ``setup.py`` file
+===============================================
+
+The models code in our application will depend on a package which is not a
+dependency of the original "tutorial" application. The original "tutorial"
+application was generated by the ``pcreate`` command; it doesn't know about our
+custom application requirements.
+
+We need to add a dependency, the ``bcrypt`` package, to our ``tutorial``
+package's ``setup.py`` file by assigning this dependency to the ``requires``
+parameter in the ``setup()`` function.
+
+Open ``tutorial/setup.py`` and edit it to look like the following:
+
+.. literalinclude:: src/models/setup.py
+ :linenos:
+ :emphasize-lines: 12
+ :language: python
+
+Only the highlighted line needs to be added.
+
+
+Running ``pip install -e .``
+============================
+
+Since a new software dependency was added, you will need to run ``pip install
+-e .`` again inside the root of the ``tutorial`` package to obtain and register
+the newly added dependency distribution.
+
+Make sure your current working directory is the root of the project (the
+directory in which ``setup.py`` lives) and execute the following command.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ cd tutorial
+ $ $VENV/bin/pip install -e .
+
+On Windows:
+
+.. code-block:: doscon
+
+ c:\pyramidtut> cd tutorial
+ c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e .
+
+Success executing this command will end with a line to the console something
+like this::
+
+ Successfully installed bcrypt-2.0.0 cffi-1.5.2 pycparser-2.14 tutorial-0.0
+
+
+Remove ``mymodel.py``
+---------------------
+
+Let's delete the file ``tutorial/models/mymodel.py``. The ``MyModel`` class is
+only a sample and we're not going to use it.
+
+
+Add ``user.py``
+---------------
+
+Create a new file ``tutorial/models/user.py`` with the following contents:
+
+.. literalinclude:: src/models/tutorial/models/user.py
+ :linenos:
+ :language: py
+
+This is a very basic model for a user who can authenticate with our wiki.
+
+We discussed briefly in the previous chapter that our models will inherit from
+an SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This will
+attach the model to our schema.
+
+As you can see, our ``User`` class has a class-level attribute
+``__tablename__`` which equals the string ``users``. Our ``User`` class will
+also have class-level attributes named ``id``, ``name``, ``password_hash``,
+and ``role`` (all instances of :class:`sqlalchemy.schema.Column`). These will
+map to columns in the ``users`` table. The ``id`` attribute will be the primary
+key in the table. The ``name`` attribute will be a text column, each value of
+which needs to be unique within the column. The ``password_hash`` is a nullable
+text attribute that will contain a securely hashed password [1]_. Finally, the
+``role`` text attribute will hold the role of the user.
+
+There are two helper methods that will help us later when using the user
+objects. The first is ``set_password`` which will take a raw password and
+transform it using bcrypt_ into an irreversible representation, a process known
+as "hashing". The second method, ``check_password``, will allow us to compare
+the hashed value of the submitted password against the hashed value of the
+password stored in the user's record in the database. If the two hashed values
+match, then the submitted password is valid, and we can authenticate the user.
+
+We hash passwords so that it is impossible to decrypt them and use them to
+authenticate in the application. If we stored passwords foolishly in clear
+text, then anyone with access to the database could retrieve any password to
+authenticate as any user.
-Open ``tutorial/tutorial/models.py`` file and edit it to look like the
-following:
-.. literalinclude:: src/models/tutorial/models.py
+Add ``page.py``
+---------------
+
+Create a new file ``tutorial/models/page.py`` with the following contents:
+
+.. literalinclude:: src/models/tutorial/models/page.py
:linenos:
:language: py
- :emphasize-lines: 20-22,24,25
-The highlighted lines are the ones that need to be changed, as well as
-removing lines that reference ``Index``.
+As you can see, our ``Page`` class is very similar to the ``User`` defined
+above, except with attributes focused on storing information about a wiki page,
+including ``id``, ``name``, and ``data``. The only new construct introduced
+here is the ``creator_id`` column, which is a foreign key referencing the
+``users`` table. Foreign keys are very useful at the schema-level, but since we
+want to relate ``User`` objects with ``Page`` objects, we also define a
+``creator`` attribute as an ORM-level mapping between the two tables.
+SQLAlchemy will automatically populate this value using the foreign key
+referencing the user. Since the foreign key has ``nullable=False``, we are
+guaranteed that an instance of ``page`` will have a corresponding
+``page.creator``, which will be a ``User`` instance.
-The first thing we've done is remove the stock ``MyModel`` class
-from the generated ``models.py`` file. The ``MyModel`` class is only a
-sample and we're not going to use it.
-Then, we added a ``Page`` class. Because this is a SQLAlchemy application,
-this class inherits from an instance of
-:func:`sqlalchemy.ext.declarative.declarative_base`.
+Edit ``models/__init__.py``
+---------------------------
-.. literalinclude:: src/models/tutorial/models.py
- :pyobject: Page
+Since we are using a package for our models, we also need to update our
+``__init__.py`` file to ensure that the models are attached to the metadata.
+
+Open the ``tutorial/models/__init__.py`` file and edit it to look like
+the following:
+
+.. literalinclude:: src/models/tutorial/models/__init__.py
:linenos:
- :language: python
+ :language: py
+ :emphasize-lines: 8,9
-As you can see, our ``Page`` class has a class level attribute
-``__tablename__`` which equals the string ``'pages'``. This means that
-SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our
-``Page`` class will also have class-level attributes named ``id``, ``name``
-and ``data`` (all instances of :class:`sqlalchemy.schema.Column`). These will
-map to columns in the ``pages`` table. The ``id`` attribute will be the
-primary key in the table. The ``name`` attribute will be a text attribute,
-each value of which needs to be unique within the column. The ``data``
-attribute is a text attribute that will hold the body of each page.
+Here we align our imports with the names of the models, ``Page`` and ``User``.
-Changing ``scripts/initializedb.py``
-------------------------------------
+
+Edit ``scripts/initializedb.py``
+--------------------------------
We haven't looked at the details of this file yet, but within the ``scripts``
directory of your ``tutorial`` package is a file named ``initializedb.py``.
Code in this file is executed whenever we run the ``initialize_tutorial_db``
-command, as we did in the installation step of this tutorial.
+command, as we did in the installation step of this tutorial [2]_.
Since we've changed our model, we need to make changes to our
``initializedb.py`` script. In particular, we'll replace our import of
-``MyModel`` with one of ``Page`` and we'll change the very end of the script
-to create a ``Page`` rather than a ``MyModel`` and add it to our
-``DBSession``.
+``MyModel`` with those of ``User`` and ``Page``. We'll also change the very end
+of the script to create two ``User`` objects (``basic`` and ``editor``) as well
+as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``.
-Open ``tutorial/tutorial/scripts/initializedb.py`` and edit it to look like
-the following:
+Open ``tutorial/scripts/initializedb.py`` and edit it to look like the
+following:
.. literalinclude:: src/models/tutorial/scripts/initializedb.py
:linenos:
:language: python
- :emphasize-lines: 14,31,36
+ :emphasize-lines: 18,44-57
+
+Only the highlighted lines need to be changed.
-Only the highlighted lines need to be changed, as well as removing the lines
-referencing ``pyramid.scripts.common`` and ``options`` under the ``main``
-function.
Installing the project and re-initializing the database
-------------------------------------------------------
-Because our model has changed, in order to reinitialize the database, we need
-to rerun the ``initialize_tutorial_db`` command to pick up the changes you've
-made to both the models.py file and to the initializedb.py file. See
+Because our model has changed, and in order to reinitialize the database, we
+need to rerun the ``initialize_tutorial_db`` command to pick up the changes
+we've made to both the models.py file and to the initializedb.py file. See
:ref:`initialize_db_wiki2` for instructions.
-Success will look something like this::
-
- 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
- 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages")
- 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
- CREATE TABLE pages (
- id INTEGER NOT NULL,
- name TEXT,
- data TEXT,
- PRIMARY KEY (id),
- UNIQUE (name)
- )
-
-
- 2015-05-24 15:34:14,545 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-24 15:34:14,546 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
- 2015-05-24 15:34:14,548 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
- 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data) VALUES (?, ?)
- 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page')
- 2015-05-24 15:34:14,550 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+Success will look something like this:
+
+.. code-block:: bash
+
+ 2016-04-09 02:49:51,711 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2016-04-09 02:49:51,711 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-04-09 02:49:51,712 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2016-04-09 02:49:51,712 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-04-09 02:49:51,713 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages")
+ 2016-04-09 02:49:51,714 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-04-09 02:49:51,714 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("users")
+ 2016-04-09 02:49:51,714 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-04-09 02:49:51,715 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
+ CREATE TABLE users (
+ id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ role TEXT NOT NULL,
+ password_hash TEXT,
+ CONSTRAINT pk_users PRIMARY KEY (id),
+ CONSTRAINT uq_users_name UNIQUE (name)
+ )
+
+
+ 2016-04-09 02:49:51,715 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-04-09 02:49:51,716 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-04-09 02:49:51,716 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
+ CREATE TABLE pages (
+ id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ data INTEGER NOT NULL,
+ creator_id INTEGER NOT NULL,
+ CONSTRAINT pk_pages PRIMARY KEY (id),
+ CONSTRAINT uq_pages_name UNIQUE (name),
+ CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id)
+ )
+
+
+ 2016-04-09 02:49:51,716 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-04-09 02:49:51,717 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-04-09 02:49:52,256 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
+ 2016-04-09 02:49:52,257 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2016-04-09 02:49:52,257 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('editor', 'editor', b'$2b$12$APUPJvI/kKxrbQPyQehkR.ggoOM6fFYCZ07SFCkWGltl1wJsKB98y')
+ 2016-04-09 02:49:52,258 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2016-04-09 02:49:52,258 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('basic', 'basic', b'$2b$12$GeFnypuQpZyxZLH.sN0akOrPdZMcQjqVTCim67u6f89lOFH/0ddc6')
+ 2016-04-09 02:49:52,259 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
+ 2016-04-09 02:49:52,259 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page', 1)
+ 2016-04-09 02:49:52,259 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+
View the application in a browser
---------------------------------
We can't. At this point, our system is in a "non-runnable" state; we'll need
to change view-related files in the next chapter to be able to start the
-application successfully. If you try to start the application (See
-:ref:`wiki2-start-the-application`), you'll wind
-up with a Python traceback on your console that ends with this exception:
+application successfully. If you try to start the application (see
+:ref:`wiki2-start-the-application`), you'll wind up with a Python traceback on
+your console that ends with this exception:
.. code-block:: text
ImportError: cannot import name MyModel
This will also happen if you attempt to run the tests.
+
+.. _bcrypt: https://pypi.python.org/pypi/bcrypt
+
+.. [1] We are using the bcrypt_ package from PyPI to hash our passwords
+ securely. There are other one-way hash algorithms for passwords if
+ bcrypt is an issue on your system. Just make sure that it's an
+ algorithm approved for storing passwords versus a generic one-way hash.
+
+.. [2] The command is named ``initialize_tutorial_db`` because of the mapping
+ defined in the ``[console_scripts]`` entry point of our project's
+ ``setup.py`` file.
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index 0b495445a..996bff88c 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -1,12 +1,14 @@
+.. _wiki2_defining_views:
+
==============
Defining Views
==============
A :term:`view callable` in a :app:`Pyramid` application is typically a simple
-Python function that accepts a single parameter named :term:`request`. A
-view callable is assumed to return a :term:`response` object.
+Python function that accepts a single parameter named :term:`request`. A view
+callable is assumed to return a :term:`response` object.
-The request object has a dictionary as an attribute named ``matchdict``. A
+The request object has a dictionary as an attribute named ``matchdict``. A
``matchdict`` maps the placeholders in the matching URL ``pattern`` to the
substrings of the path in the :term:`request` URL. For instance, if a call to
:meth:`pyramid.config.Configurator.add_route` has the pattern ``/{one}/{two}``,
@@ -14,13 +16,13 @@ and a user visits ``http://example.com/foo/bar``, our pattern would be matched
against ``/foo/bar`` and the ``matchdict`` would look like ``{'one':'foo',
'two':'bar'}``.
-Declaring Dependencies in Our ``setup.py`` File
-===============================================
-The view code in our application will depend on a package which is not a
-dependency of the original "tutorial" application. The original "tutorial"
-application was generated by the ``pcreate`` command; it doesn't know
-about our custom application requirements.
+Adding the ``docutils`` dependency
+==================================
+
+Remember in the previous chapter we added a new dependency of the ``bcrypt``
+package. Again, the view code in our application will depend on a package which
+is not a dependency of the original "tutorial" application.
We need to add a dependency on the ``docutils`` package to our ``tutorial``
package's ``setup.py`` file by assigning this dependency to the ``requires``
@@ -30,109 +32,164 @@ Open ``tutorial/setup.py`` and edit it to look like the following:
.. literalinclude:: src/views/setup.py
:linenos:
- :emphasize-lines: 20
+ :emphasize-lines: 13
:language: python
Only the highlighted line needs to be added.
-Running ``setup.py develop``
-============================
+Again, as we did in the previous chapter, the dependency now needs to be
+installed, so re-run the ``$VENV/bin/pip install -e .`` command.
-Since a new software dependency was added, you will need to run ``python
-setup.py develop`` again inside the root of the ``tutorial`` package to obtain
-and register the newly added dependency distribution.
-Make sure your current working directory is the root of the project (the
-directory in which ``setup.py`` lives) and execute the following command.
+Static assets
+-------------
+
+Our templates name static assets, including CSS and images. We don't need
+to create these files within our package's ``static`` directory because they
+were provided at the time we created the project.
+
+As an example, the CSS file will be accessed via
+``http://localhost:6543/static/theme.css`` by virtue of the call to the
+``add_static_view`` directive we've made in the ``routes.py`` file. Any number
+and type of static assets can be placed in this directory (or subdirectories)
+and are just referred to by URL or by using the convenience method
+``static_url``, e.g., ``request.static_url('<package>:static/foo.css')`` within
+templates.
-On UNIX:
-.. code-block:: text
+Adding routes to ``routes.py``
+==============================
- $ cd tutorial
- $ $VENV/bin/python setup.py develop
+This is the `URL Dispatch` tutorial, so let's start by adding some URL patterns
+to our app. Later we'll attach views to handle the URLs.
-On Windows:
+The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route`
+calls which serve to add routes to our application. First we'll get rid of the
+existing route created by the template using the name ``'home'``. It's only an
+example and isn't relevant to our application.
-.. code-block:: text
+We then need to add four calls to ``add_route``. Note that the *ordering* of
+these declarations is very important. Route declarations are matched in the
+order they're registered.
- c:\pyramidtut> cd tutorial
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+#. Add a declaration which maps the pattern ``/`` (signifying the root URL) to
+ the route named ``view_wiki``. In the next step, we will map it to our
+ ``view_wiki`` view callable by virtue of the ``@view_config`` decorator
+ attached to the ``view_wiki`` view function, which in turn will be indicated
+ by ``route_name='view_wiki'``.
-Success executing this command will end with a line to the console something
-like::
+#. Add a declaration which maps the pattern ``/{pagename}`` to the route named
+ ``view_page``. This is the regular view for a page. Again, in the next step,
+ we will map it to our ``view_page`` view callable by virtue of the
+ ``@view_config`` decorator attached to the ``view_page`` view function,
+ whin in turn will be indicated by ``route_name='view_page'``.
- Finished processing dependencies for tutorial==0.0
+#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the
+ route named ``add_page``. This is the add view for a new page. We will map
+ it to our ``add_page`` view callable by virtue of the ``@view_config``
+ decorator attached to the ``add_page`` view function, which in turn will be
+ indicated by ``route_name='add_page'``.
-Adding view functions in ``views.py``
-=====================================
+#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the
+ route named ``edit_page``. This is the edit view for a page. We will map it
+ to our ``edit_page`` view callable by virtue of the ``@view_config``
+ decorator attached to the ``edit_page`` view function, which in turn will be
+ indicated by ``route_name='edit_page'``.
-It's time for a major change. Open ``tutorial/tutorial/views.py`` and edit it
-to look like the following:
+As a result of our edits, the ``routes.py`` file should look like the
+following:
-.. literalinclude:: src/views/tutorial/views.py
+.. literalinclude:: src/views/tutorial/routes.py
:linenos:
+ :emphasize-lines: 3-6
:language: python
- :emphasize-lines: 1-7,14,16-72
+
+The highlighted lines are the ones that need to be added or edited.
+
+.. warning::
+
+ The order of the routes is important! If you placed
+ ``/{pagename}/edit_page`` *before* ``/add_page/{pagename}``, then we would
+ never be able to add pages. This is because the first route would always
+ match a request to ``/add_page/edit_page`` whereas we want ``/add_page/..``
+ to have priority. This isn't a huge problem in this particular app because
+ wiki pages are always camel case, but it's important to be aware of this
+ behavior in your own apps.
+
+
+Adding view functions in ``views/default.py``
+=============================================
+
+It's time for a major change. Open ``tutorial/views/default.py`` and
+edit it to look like the following:
+
+.. literalinclude:: src/views/tutorial/views/default.py
+ :linenos:
+ :language: python
+ :emphasize-lines: 1-9,12-
The highlighted lines need to be added or edited.
-We added some imports and created a regular expression to find "WikiWords".
+We added some imports, and created a regular expression to find "WikiWords".
We got rid of the ``my_view`` view function and its decorator that was added
when we originally rendered the ``alchemy`` scaffold. It was only an example
-and isn't relevant to our application.
+and isn't relevant to our application. We also deleted the ``db_err_msg``
+string.
-Then we added four :term:`view callable` functions to our ``views.py``
-module:
+Then we added four :term:`view callable` functions to our ``views/default.py``
+module, as mentioned in the previous step:
* ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL.
* ``view_page()`` - Displays an individual page.
-* ``add_page()`` - Allows the user to add a page.
* ``edit_page()`` - Allows the user to edit a page.
+* ``add_page()`` - Allows the user to add a page.
We'll describe each one briefly in the following sections.
.. note::
- There is nothing special about the filename ``views.py``. A project may
- have many view callables throughout its codebase in arbitrarily named
- files. Files implementing view callables often have ``view`` in their
- filenames (or may live in a Python subpackage of your application package
- named ``views``), but this is only by convention.
+ There is nothing special about the filename ``default.py`` exept that it is a
+ Python module. A project may have many view callables throughout its codebase
+ in arbitrarily named modules. Modules implementing view callables often have
+ ``view`` in their name (or may live in a Python subpackage of your
+ application package named ``views``, as in our case), but this is only by
+ convention, not a requirement.
+
The ``view_wiki`` view function
-------------------------------
Following is the code for the ``view_wiki`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 20-24
- :lineno-start: 20
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 17-20
+ :lineno-match:
:linenos:
:language: python
``view_wiki()`` is the :term:`default view` that gets called when a request is
-made to the root URL of our wiki. It always redirects to an URL which
+made to the root URL of our wiki. It always redirects to a URL which
represents the path to our "FrontPage".
The ``view_wiki`` view callable always redirects to the URL of a Page resource
named "FrontPage". To do so, it returns an instance of the
:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement
the :class:`pyramid.interfaces.IResponse` interface, like
-:class:`pyramid.response.Response` does). It uses the
-:meth:`pyramid.request.Request.route_url` API to construct an URL to the
+:class:`pyramid.response.Response`). It uses the
+:meth:`pyramid.request.Request.route_url` API to construct a URL to the
``FrontPage`` page (i.e., ``http://localhost:6543/FrontPage``), and uses it as
the "location" of the ``HTTPFound`` response, forming an HTTP redirect.
+
The ``view_page`` view function
-------------------------------
Here is the code for the ``view_page`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 25-45
- :lineno-start: 25
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 22-42
+ :lineno-match:
:linenos:
:language: python
@@ -141,83 +198,59 @@ Here is the code for the ``view_page`` view function and its decorator:
``Page`` model object) as HTML. Then it substitutes an HTML anchor for each
*WikiWord* reference in the rendered HTML using a compiled regular expression.
-The curried function named ``check`` is used as the first argument to
+The curried function named ``add_link`` is used as the first argument to
``wikiwords.sub``, indicating that it should be called to provide a value for
each WikiWord match found in the content. If the wiki already contains a
-page with the matched WikiWord name, ``check()`` generates a view
+page with the matched WikiWord name, ``add_link()`` generates a view
link to be used as the substitution value and returns it. If the wiki does
-not already contain a page with the matched WikiWord name, ``check()``
+not already contain a page with the matched WikiWord name, ``add_link()``
generates an "add" link as the substitution value and returns it.
As a result, the ``content`` variable is now a fully formed bit of HTML
containing various view and add links for WikiWords based on the content of
our current page object.
-We then generate an edit URL because it's easier to do here than in the
+We then generate an edit URL, because it's easier to do here than in the
template, and we return a dictionary with a number of arguments. The fact that
``view_page()`` returns a dictionary (as opposed to a :term:`response` object)
is a cue to :app:`Pyramid` that it should try to use a :term:`renderer`
associated with the view configuration to render a response. In our case, the
-renderer used will be the ``templates/view.pt`` template, as indicated in the
-``@view_config`` decorator that is applied to ``view_page()``.
+renderer used will be the ``view.jinja2`` template, as indicated in
+the ``@view_config`` decorator that is applied to ``view_page()``.
-The ``add_page`` view function
-------------------------------
+If the page does not exist, then we need to handle that by raising a
+:class:`pyramid.httpexceptions.HTTPNotFound` to trigger our 404 handling,
+defined in ``tutorial/views/notfound.py``.
-Here is the code for the ``add_page`` view function and its decorator:
-
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 47-58
- :lineno-start: 47
- :linenos:
- :language: python
-
-``add_page()`` is invoked when a user clicks on a *WikiWord* which
-isn't yet represented as a page in the system. The ``check`` function
-within the ``view_page`` view generates URLs to this view.
-``add_page()`` also acts as a handler for the form that is generated
-when we want to add a page object. The ``matchdict`` attribute of the
-request passed to the ``add_page()`` view will have the values we need
-to construct URLs and find model objects.
-
-The ``matchdict`` will have a ``'pagename'`` key that matches the name of
-the page we'd like to add. If our add view is invoked via,
-e.g., ``http://localhost:6543/add_page/SomeName``, the value for
-``'pagename'`` in the ``matchdict`` will be ``'SomeName'``.
+.. note::
-If the view execution *is* a result of a form submission (i.e., the expression
-``'form.submitted' in request.params`` is ``True``), we grab the page body
-from the form data, create a Page object with this page body and the name
-taken from ``matchdict['pagename']``, and save it into the database using
-``DBSession.add``. We then redirect back to the ``view_page`` view for the
-newly created page.
+ Using ``raise`` versus ``return`` with the HTTP exceptions is an important
+ distinction that can commonly mess people up. In
+ ``tutorial/views/notfound.py`` there is an :term:`exception view`
+ registered for handling the ``HTTPNotFound`` exception. Exception views are
+ only triggered for raised exceptions. If the ``HTTPNotFound`` is returned,
+ then it has an internal "stock" template that it will use to render itself
+ as a response. If you aren't seeing your exception view being executed, this
+ is most likely the problem! See :ref:`special_exceptions_in_callables` for
+ more information about exception views.
-If the view execution is *not* a result of a form submission (i.e., the
-expression ``'form.submitted' in request.params`` is ``False``), the view
-callable renders a template. To do so, it generates a ``save_url`` which the
-template uses as the form post URL during rendering. We're lazy here, so
-we're going to use the same template (``templates/edit.pt``) for the add
-view as well as the page edit view. To do so we create a dummy Page object
-in order to satisfy the edit form's desire to have *some* page object
-exposed as ``page``. :app:`Pyramid` will render the template associated
-with this view to a response.
The ``edit_page`` view function
-------------------------------
Here is the code for the ``edit_page`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 60-72
- :lineno-start: 60
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 44-56
+ :lineno-match:
:linenos:
:language: python
-``edit_page()`` is invoked when a user clicks the "Edit this
-Page" button on the view form. It renders an edit form but it also acts as
-the handler for the form it renders. The ``matchdict`` attribute of the
-request passed to the ``edit_page`` view will have a ``'pagename'`` key
-matching the name of the page the user wants to edit.
+``edit_page()`` is invoked when a user clicks the "Edit this Page" button on
+the view form. It renders an edit form, but it also acts as the handler for the
+form which it renders. The ``matchdict`` attribute of the request passed to the
+``edit_page`` view will have a ``'pagename'`` key matching the name of the page
+that the user wants to edit.
If the view execution *is* a result of a form submission (i.e., the expression
``'form.submitted' in request.params`` is ``True``), the view grabs the
@@ -230,120 +263,190 @@ expression ``'form.submitted' in request.params`` is ``False``), the view
simply renders the edit form, passing the page object and a ``save_url``
which will be used as the action of the generated form.
+.. note::
+
+ Since our ``request.dbsession`` defined in the previous chapter is
+ registered with the ``pyramid_tm`` transaction manager, any changes we make
+ to objects managed by the that session will be committed automatically. In
+ the event that there was an error (even later, in our template code), the
+ changes would be aborted. This means the view itself does not need to
+ concern itself with commit/rollback logic.
+
+
+The ``add_page`` view function
+------------------------------
+
+Here is the code for the ``add_page`` view function and its decorator:
+
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 58-
+ :lineno-match:
+ :linenos:
+ :language: python
+
+``add_page()`` is invoked when a user clicks on a *WikiWord* which isn't yet
+represented as a page in the system. The ``add_link`` function within the
+``view_page`` view generates URLs to this view. ``add_page()`` also acts as a
+handler for the form that is generated when we want to add a page object. The
+``matchdict`` attribute of the request passed to the ``add_page()`` view will
+have the values we need to construct URLs and find model objects.
+
+The ``matchdict`` will have a ``'pagename'`` key that matches the name of the
+page we'd like to add. If our add view is invoked via, for example,
+``http://localhost:6543/add_page/SomeName``, the value for ``'pagename'`` in
+the ``matchdict`` will be ``'SomeName'``.
+
+Next a check is performed to determine whether the ``Page`` already exists in
+the database. If it already exists, then the client is redirected to the
+``edit_page`` view, else we continue to the next check.
+
+If the view execution *is* a result of a form submission (i.e., the expression
+``'form.submitted' in request.params`` is ``True``), we grab the page body from
+the form data, create a Page object with this page body and the name taken from
+``matchdict['pagename']``, and save it into the database using
+``request.dbession.add``. Since we have not yet covered authentication, we
+don't have a logged-in user to add as the page's ``creator``. Until we get to
+that point in the tutorial, we'll just assume that all pages are created by the
+``editor`` user. Thus we query for that object, and set it on ``page.creator``.
+Finally, we redirect the client back to the ``view_page`` view for the newly
+created page.
+
+If the view execution is *not* a result of a form submission (i.e., the
+expression ``'form.submitted' in request.params`` is ``False``), the view
+callable renders a template. To do so, it generates a ``save_url`` which the
+template uses as the form post URL during rendering. We're lazy here, so
+we're going to use the same template (``templates/edit.jinja2``) for the add
+view as well as the page edit view. To do so we create a dummy ``Page`` object
+in order to satisfy the edit form's desire to have *some* page object
+exposed as ``page``. :app:`Pyramid` will render the template associated
+with this view to a response.
+
+
Adding templates
================
The ``view_page``, ``add_page`` and ``edit_page`` views that we've added
-reference a :term:`template`. Each template is a :term:`Chameleon`
-:term:`ZPT` template. These templates will live in the ``templates``
-directory of our tutorial package. Chameleon templates must have a ``.pt``
-extension to be recognized as such.
+reference a :term:`template`. Each template is a :term:`Jinja2` template.
+These templates will live in the ``templates`` directory of our tutorial
+package. Jinja2 templates must have a ``.jinja2`` extension to be recognized
+as such.
+
-The ``view.pt`` template
-------------------------
+The ``layout.jinja2`` template
+------------------------------
-Create ``tutorial/tutorial/templates/view.pt`` and add the following
-content:
+Update ``tutorial/templates/layout.jinja2`` with the following content, as
+indicated by the emphasized lines:
-.. literalinclude:: src/views/tutorial/templates/view.pt
+.. literalinclude:: src/views/tutorial/templates/layout.jinja2
:linenos:
+ :emphasize-lines: 11,35-36
:language: html
-This template is used by ``view_page()`` for displaying a single
-wiki page. It includes:
+Since we're using a templating engine, we can factor common boilerplate out of
+our page templates into reusable components. One method for doing this is
+template inheritance via blocks.
-- A ``div`` element that is replaced with the ``content`` value provided by
- the view (lines 36-38). ``content`` contains HTML, so the ``structure``
- keyword is used to prevent escaping it (i.e., changing ">" to "&gt;", etc.)
-- A link that points at the "edit" URL which invokes the ``edit_page`` view
- for the page being viewed (lines 40-42).
+- We have defined two placeholders in the layout template where a child
+ template can override the content. These blocks are named ``subtitle`` (line
+ 11) and ``content`` (line 36).
+- Please refer to the Jinja2_ documentation for more information about template
+ inheritance.
-The ``edit.pt`` template
-------------------------
-Create ``tutorial/tutorial/templates/edit.pt`` and add the following
-content:
+The ``view.jinja2`` template
+----------------------------
-.. literalinclude:: src/views/tutorial/templates/edit.pt
+Create ``tutorial/templates/view.jinja2`` and add the following content:
+
+.. literalinclude:: src/views/tutorial/templates/view.jinja2
:linenos:
:language: html
-This template is used by ``add_page()`` and ``edit_page()`` for adding and
-editing a wiki page. It displays a page containing a form that includes:
+This template is used by ``view_page()`` for displaying a single wiki page.
-- A 10 row by 60 column ``textarea`` field named ``body`` that is filled
- with any existing page data when it is rendered (line 45).
-- A submit button that has the name ``form.submitted`` (line 48).
+- We begin by extending the ``layout.jinja2`` template defined above, which
+ provides the skeleton of the page (line 1).
+- We override the ``subtitle`` block from the base layout, inserting the page
+ name into the page's title (line 3).
+- We override the ``content`` block from the base layout to insert our markup
+ into the body (lines 5-18).
+- We use a variable that is replaced with the ``content`` value provided by the
+ view (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to
+ prevent escaping it (e.g., changing ">" to "&gt;").
+- We create a link that points at the "edit" URL, which when clicked invokes
+ the ``edit_page`` view for the requested page (line 9).
-The form POSTs back to the ``save_url`` argument supplied by the view (line
-43). The view will use the ``body`` and ``form.submitted`` values.
-.. note:: Our templates use a ``request`` object that none of our tutorial
- views return in their dictionary. ``request`` is one of several names that
- are available "by default" in a template when a template renderer is used.
- See :ref:`renderer_system_values` for information about other names that
- are available by default when a template is used as a renderer.
+The ``edit.jinja2`` template
+----------------------------
-Static Assets
--------------
+Create ``tutorial/templates/edit.jinja2`` and add the following content:
-Our templates name static assets, including CSS and images. We don't need
-to create these files within our package's ``static`` directory because they
-were provided at the time we created the project.
+.. literalinclude:: src/views/tutorial/templates/edit.jinja2
+ :linenos:
+ :emphasize-lines: 1,3,12,14,17
+ :language: html
-As an example, the CSS file will be accessed via
-``http://localhost:6543/static/theme.css`` by virtue of the call to the
-``add_static_view`` directive we've made in the ``__init__.py`` file. Any
-number and type of static assets can be placed in this directory (or
-subdirectories) and are just referred to by URL or by using the convenience
-method ``static_url``, e.g.,
-``request.static_url('<package>:static/foo.css')`` within templates.
-
-Adding Routes to ``__init__.py``
-================================
-
-The ``__init__.py`` file contains
-:meth:`pyramid.config.Configurator.add_route` calls which serve to add routes
-to our application. First, we’ll get rid of the existing route created by
-the template using the name ``'home'``. It’s only an example and isn’t
-relevant to our application.
-
-We then need to add four calls to ``add_route``. Note that the *ordering* of
-these declarations is very important. ``route`` declarations are matched in
-the order they're found in the ``__init__.py`` file.
-
-#. Add a declaration which maps the pattern ``/`` (signifying the root URL)
- to the route named ``view_wiki``. It maps to our ``view_wiki`` view
- callable by virtue of the ``@view_config`` attached to the ``view_wiki``
- view function indicating ``route_name='view_wiki'``.
+This template serves two use cases. It is used by ``add_page()`` and
+``edit_page()`` for adding and editing a wiki page. It displays a page
+containing a form and which provides the following:
-#. Add a declaration which maps the pattern ``/{pagename}`` to the route named
- ``view_page``. This is the regular view for a page. It maps
- to our ``view_page`` view callable by virtue of the ``@view_config``
- attached to the ``view_page`` view function indicating
- ``route_name='view_page'``.
+- Again, we extend the ``layout.jinja2`` template, which provides the skeleton
+ of the page (line 1).
+- Override the ``subtitle`` block to affect the ``<title>`` tag in the
+ ``head`` of the page (line 3).
+- A 10-row by 60-column ``textarea`` field named ``body`` that is filled with
+ any existing page data when it is rendered (line 14).
+- A submit button that has the name ``form.submitted`` (line 17).
+- The form POSTs back to the ``save_url`` argument supplied by the view (line
+ 12). The view will use the ``body`` and ``form.submitted`` values.
-#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the
- route named ``add_page``. This is the add view for a new page. It maps
- to our ``add_page`` view callable by virtue of the ``@view_config``
- attached to the ``add_page`` view function indicating
- ``route_name='add_page'``.
-#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the
- route named ``edit_page``. This is the edit view for a page. It maps
- to our ``edit_page`` view callable by virtue of the ``@view_config``
- attached to the ``edit_page`` view function indicating
- ``route_name='edit_page'``.
+The ``404.jinja2`` template
+---------------------------
+
+Replace ``tutorial/templates/404.jinja2`` with the following content:
+
+.. literalinclude:: src/views/tutorial/templates/404.jinja2
+ :linenos:
+ :language: html
-As a result of our edits, the ``__init__.py`` file should look
-something like:
+This template is linked from the ``notfound_view`` defined in
+``tutorial/views/notfound.py`` as shown here:
-.. literalinclude:: src/views/tutorial/__init__.py
+.. literalinclude:: src/views/tutorial/views/notfound.py
:linenos:
- :emphasize-lines: 19-22
+ :emphasize-lines: 6
:language: python
-The highlighted lines are the ones that need to be added or edited.
+There are several important things to note about this configuration:
+
+- The ``notfound_view`` in the above snippet is called an
+ :term:`exception view`. For more information see
+ :ref:`special_exceptions_in_callables`.
+- The ``notfound_view`` sets the response status to 404. It's possible
+ to affect the response object used by the renderer via
+ :ref:`request_response_attr`.
+- The ``notfound_view`` is registered as an exception view and will be invoked
+ **only** if ``pyramid.httpexceptions.HTTPNotFound`` is raised as an
+ exception. This means it will not be invoked for any responses returned
+ from a view normally. For example, on line 27 of
+ ``tutorial/views/default.py`` the exception is raised which will trigger
+ the view.
+
+Finally, we may delete the ``tutorial/templates/mytemplate.jinja2`` template
+that was provided by the ``alchemy`` scaffold, as we have created our own
+templates for the wiki.
+
+.. note::
+
+ Our templates use a ``request`` object that none of our tutorial
+ views return in their dictionary. ``request`` is one of several names that
+ are available "by default" in a template when a template renderer is used.
+ See :ref:`renderer_system_values` for information about other names that
+ are available by default when a template is used as a renderer.
+
Viewing the application in a browser
====================================
@@ -355,15 +458,22 @@ each of the following URLs, checking that the result is as expected:
- http://localhost:6543/ invokes the ``view_wiki`` view. This always
redirects to the ``view_page`` view of the ``FrontPage`` page object.
-- http://localhost:6543/FrontPage invokes the ``view_page`` view of the front
- page object.
+- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
+ ``FrontPage`` page object.
+
+- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for
+ the ``FrontPage`` page object.
-- http://localhost:6543/FrontPage/edit_page invokes the edit view for the
- front page object.
+- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for
+ a page. If the page already exists, then it redirects the user to the
+ ``edit_page`` view for the page object.
-- http://localhost:6543/add_page/SomePageName invokes the add view for a page.
+- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view
+ for an existing page, or generates an error if the page does not exist.
- To generate an error, visit http://localhost:6543/foobars/edit_page which
will generate a ``NoResultFound: No row was found for one()`` error. You'll
see an interactive traceback facility provided by
:term:`pyramid_debugtoolbar`.
+
+.. _jinja2: http://jinja.pocoo.org/
diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst
index 52f2ce7a5..523a6e6d8 100644
--- a/docs/tutorials/wiki2/design.rst
+++ b/docs/tutorials/wiki2/design.rst
@@ -1,150 +1,162 @@
+.. _wiki2_design:
+
======
Design
======
-Following is a quick overview of the design of our wiki application, to help
-us understand the changes that we will be making as we work through the
-tutorial.
+Following is a quick overview of the design of our wiki application to help us
+understand the changes that we will be making as we work through the tutorial.
Overall
--------
+=======
We choose to use :term:`reStructuredText` markup in the wiki text. Translation
from reStructuredText to HTML is provided by the widely used ``docutils``
-Python module. We will add this module in the dependency list on the project
+Python module. We will add this module to the dependency list in the project's
``setup.py`` file.
Models
-------
+======
-We'll be using a SQLite database to hold our wiki data, and we'll be using
+We'll be using an SQLite database to hold our wiki data, and we'll be using
:term:`SQLAlchemy` to access the data in this database.
-Within the database, we define a single table named `pages`, whose elements
-will store the wiki pages. There are two columns: `name` and `data`.
+Within the database, we will define two tables:
+
+- The `users` table which will store the `id`, `name`, `password_hash` and
+ `role` of each wiki user.
+- The `pages` table, whose elements will store the wiki pages.
+ There are four columns: `id`, `name`, `data` and `creator_id`.
-URLs like ``/PageName`` will try to find an element in
-the table that has a corresponding name.
+There is a one-to-many relationship between `users` and `pages` tracking
+the user who created each wiki page defined by the `creator_id` column on the
+`pages` table.
-To add a page to the wiki, a new row is created and the text
-is stored in `data`.
+URLs like ``/PageName`` will try to find an element in the `pages` table that
+has a corresponding name.
+
+To add a page to the wiki, a new row is created and the text is stored in
+`data`.
A page named ``FrontPage`` containing the text *This is the front page*, will
be created when the storage is initialized, and will be used as the wiki home
page.
-Views
------
+Wiki Views
+==========
-There will be three views to handle the normal operations of adding,
-editing, and viewing wiki pages, plus one view for the wiki front page.
-Two templates will be used, one for viewing, and one for both adding
-and editing wiki pages.
+There will be three views to handle the normal operations of adding, editing,
+and viewing wiki pages, plus one view for the wiki front page. Two templates
+will be used, one for viewing, and one for both adding and editing wiki pages.
-The default templating systems in :app:`Pyramid` are
-:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of
-:term:`ZPT`, which is an XML-based templating language. Mako is a
-non-XML-based templating language. Because we had to pick one,
-we chose Chameleon for this tutorial.
+As of version 1.5 :app:`Pyramid` no longer ships with templating systems. In
+this tutorial, we will use :term:`Jinja2`. Jinja2 is a modern and
+designer-friendly templating language for Python, modeled after Django's
+templates.
Security
---------
-
-We'll eventually be adding security to our application. The components we'll
-use to do this are below.
-
-- USERS, a dictionary mapping :term:`userids <userid>` to their
- corresponding passwords.
-
-- GROUPS, a dictionary mapping :term:`userids <userid>` to a
- list of groups to which they belong.
-
-- ``groupfinder``, an *authorization callback* that looks up USERS and
- GROUPS. It will be provided in a new ``security.py`` file.
-
-- An :term:`ACL` is attached to the root :term:`resource`. Each row below
- details an :term:`ACE`:
-
- +----------+----------------+----------------+
- | Action | Principal | Permission |
- +==========+================+================+
- | Allow | Everyone | View |
- +----------+----------------+----------------+
- | Allow | group:editors | Edit |
- +----------+----------------+----------------+
-
-- Permission declarations are added to the views to assert the security
- policies as each request is handled.
-
-Two additional views and one template will handle the login and
-logout tasks.
+========
+
+We'll eventually be adding security to our application. To do this, we'll
+be using a very simple role-based security model. We'll assign a single
+role category to each user in our system.
+
+`basic`
+ An authenticated user who can view content and create new pages. A `basic`
+ user may also edit the pages they have created but not pages created by
+ other users.
+
+`editor`
+ An authenticated user who can create and edit any content in the system.
+
+In order to accomplish this we'll need to define an authentication policy
+which can identify users by their :term:`userid` and role. Then we'll
+need to define a page :term:`resource` which contains the appropriate
+:term:`ACL`:
+
++----------+--------------------+----------------+
+| Action | Principal | Permission |
++==========+====================+================+
+| Allow | Everyone | view |
++----------+--------------------+----------------+
+| Allow | group:basic | create |
++----------+--------------------+----------------+
+| Allow | group:editors | edit |
++----------+--------------------+----------------+
+| Allow | <creator of page> | edit |
++----------+--------------------+----------------+
+
+Permission declarations will be added to the views to assert the security
+policies as each request is handled.
+
+On the security side of the application there are two additional views for
+handling login and logout as well as two exception views for handling
+invalid access attempts and unhandled URLs.
Summary
--------
-
-The URL, actions, template and permission associated to each view are
-listed in the following table:
-
-+----------------------+-----------------------+-------------+------------+------------+
-| URL | Action | View | Template | Permission |
-| | | | | |
-+======================+=======================+=============+============+============+
-| / | Redirect to | view_wiki | | |
-| | /FrontPage | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /PageName | Display existing | view_page | view.pt | view |
-| | page [2]_ | [1]_ | | |
-| | | | | |
-| | | | | |
-| | | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /PageName/edit_page | Display edit form | edit_page | edit.pt | edit |
-| | with existing | | | |
-| | content. | | | |
-| | | | | |
-| | If the form was | | | |
-| | submitted, redirect | | | |
-| | to /PageName | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /add_page/PageName | Create the page | add_page | edit.pt | edit |
-| | *PageName* in | | | |
-| | storage, display | | | |
-| | the edit form | | | |
-| | without content. | | | |
-| | | | | |
-| | If the form was | | | |
-| | submitted, | | | |
-| | redirect to | | | |
-| | /PageName | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /login | Display login form, | login | login.pt | |
-| | Forbidden [3]_ | | | |
-| | | | | |
-| | If the form was | | | |
-| | submitted, | | | |
-| | authenticate. | | | |
-| | | | | |
-| | - If authentication | | | |
-| | succeeds, | | | |
-| | redirect to the | | | |
-| | page that we | | | |
-| | came from. | | | |
-| | | | | |
-| | - If authentication | | | |
-| | fails, display | | | |
-| | login form with | | | |
-| | "login failed" | | | |
-| | message. | | | |
-| | | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /logout | Redirect to | logout | | |
-| | /FrontPage | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-
-.. [1] This is the default view for a Page context
- when there is no view name.
-.. [2] Pyramid will return a default 404 Not Found page
- if the page *PageName* does not exist yet.
-.. [3] ``pyramid.exceptions.Forbidden`` is reached when a
- user tries to invoke a view that is
- not authorized by the authorization policy.
+=======
+
+The URL, actions, template, and permission associated to each view are listed
+in the following table:
+
++----------------------+-----------------------+-------------+----------------+------------+
+| URL | Action | View | Template | Permission |
+| | | | | |
++======================+=======================+=============+================+============+
+| / | Redirect to | view_wiki | | |
+| | /FrontPage | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /PageName | Display existing | view_page | view.jinja2 | view |
+| | page [2]_ | [1]_ | | |
+| | | | | |
+| | | | | |
+| | | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /PageName/edit_page | Display edit form | edit_page | edit.jinja2 | edit |
+| | with existing | | | |
+| | content. | | | |
+| | | | | |
+| | If the form was | | | |
+| | submitted, redirect | | | |
+| | to /PageName | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /add_page/PageName | Create the page | add_page | edit.jinja2 | create |
+| | *PageName* in | | | |
+| | storage, display | | | |
+| | the edit form | | | |
+| | without content. | | | |
+| | | | | |
+| | If the form was | | | |
+| | submitted, | | | |
+| | redirect to | | | |
+| | /PageName | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /login | Display login form, | login | login.jinja2 | |
+| | Forbidden [3]_ | | | |
+| | | | | |
+| | If the form was | | | |
+| | submitted, | | | |
+| | authenticate. | | | |
+| | | | | |
+| | - If authentication | | | |
+| | succeeds, | | | |
+| | redirect to the | | | |
+| | page from which | | | |
+| | we came. | | | |
+| | | | | |
+| | - If authentication | | | |
+| | fails, display | | | |
+| | login form with | | | |
+| | "login failed" | | | |
+| | message. | | | |
+| | | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+| /logout | Redirect to | logout | | |
+| | /FrontPage | | | |
++----------------------+-----------------------+-------------+----------------+------------+
+
+.. [1] This is the default view for a Page context when there is no view name.
+.. [2] Pyramid will return a default 404 Not Found page if the page *PageName*
+ does not exist yet.
+.. [3] ``pyramid.exceptions.Forbidden`` is reached when a user tries to invoke
+ a view that is not authorized by the authorization policy.
diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst
index fee50a1cf..f264448b0 100644
--- a/docs/tutorials/wiki2/distributing.rst
+++ b/docs/tutorials/wiki2/distributing.rst
@@ -1,22 +1,23 @@
+.. _wiki2_distributing_your_application:
+
=============================
Distributing Your Application
=============================
Once your application works properly, you can create a "tarball" from it by
using the ``setup.py sdist`` command. The following commands assume your
-current working directory is the ``tutorial`` package we've created and that
-the parent directory of the ``tutorial`` package is a virtualenv representing
-a :app:`Pyramid` environment.
+current working directory contains the ``tutorial`` package and the
+``setup.py`` file.
On UNIX:
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/python setup.py sdist
On Windows:
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut> %VENV%\Scripts\python setup.py sdist
@@ -27,8 +28,7 @@ The output of such a command will be something like:
running sdist
# .. more output ..
creating dist
- tar -cf dist/tutorial-0.0.tar tutorial-0.0
- gzip -f9 dist/tutorial-0.0.tar
+ Creating tar archive
removing 'tutorial-0.0' (and everything under it)
Note that this command creates a tarball in the "dist" subdirectory named
diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst
index 0a3873dcd..18e9f552e 100644
--- a/docs/tutorials/wiki2/index.rst
+++ b/docs/tutorials/wiki2/index.rst
@@ -1,15 +1,15 @@
.. _bfg_sql_wiki_tutorial:
-SQLAlchemy + URL Dispatch Wiki Tutorial
+SQLAlchemy + URL dispatch wiki tutorial
=======================================
-This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based
+This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based
:app:`Pyramid` application to a developer familiar with Python. When the
-tutorial is finished, the developer will have created a basic Wiki
-application with authentication.
+tutorial is finished, the developer will have created a basic wiki
+application with authentication and authorization.
-For cut and paste purposes, the source code for all stages of this
-tutorial can be browsed on GitHub at `docs/tutorials/wiki2/src
+For cut and paste purposes, the source code for all stages of this tutorial can
+be browsed on GitHub at `docs/tutorials/wiki2/src
<https://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src>`_,
which corresponds to the same location if you have Pyramid sources.
@@ -22,6 +22,7 @@ which corresponds to the same location if you have Pyramid sources.
basiclayout
definingmodels
definingviews
+ authentication
authorization
tests
distributing
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 595dbd940..f4676345e 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -1,17 +1,19 @@
+.. _wiki2_installation:
+
============
Installation
============
Before you begin
-================
+----------------
This tutorial assumes that you have already followed the steps in
-:ref:`installing_chapter`, except **do not create a virtualenv or install
-Pyramid**. Thereby you will satisfy the following requirements.
+:ref:`installing_chapter`, except **do not create a virtual environment or
+install Pyramid**. Thereby you will satisfy the following requirements.
+
+* A Python interpreter is installed on your operating system.
+* You've satisfied the :ref:`requirements-for-installing-packages`.
-* Python interpreter is installed on your operating system
-* :term:`setuptools` or :term:`distribute` is installed
-* :term:`virtualenv` is installed
Create directory to contain the project
---------------------------------------
@@ -21,55 +23,73 @@ We need a workspace for our project files.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ mkdir ~/pyramidtut
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\> mkdir pyramidtut
+
Create and use a virtual Python environment
-------------------------------------------
-Next let's create a `virtualenv` workspace for our project. We will
-use the `VENV` environment variable instead of the absolute path of the
-virtual environment.
+Next let's create a virtual environment workspace for our project. We will use
+the ``VENV`` environment variable instead of the absolute path of the virtual
+environment.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ export VENV=~/pyramidtut
- $ virtualenv $VENV
- New python executable in /home/foo/env/bin/python
- Installing setuptools.............done.
+ $ python3 -m venv $VENV
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\> set VENV=c:\pyramidtut
-Versions of Python use different paths, so you will need to adjust the
+Each version of Python uses different paths, so you will need to adjust the
path to the command for your Python version.
Python 2.7:
-.. code-block:: text
+.. code-block:: doscon
c:\> c:\Python27\Scripts\virtualenv %VENV%
-Python 3.3:
+Python 3.5:
+
+.. code-block:: doscon
+
+ c:\> c:\Python35\Scripts\python -m venv %VENV%
+
+
+Upgrade ``pip`` and ``setuptools`` in the virtual environment
+-------------------------------------------------------------
+
+On UNIX
+^^^^^^^
+
+.. code-block:: bash
+
+ $ $VENV/bin/pip install --upgrade pip setuptools
-.. code-block:: text
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ c:\> %VENV%\Scripts\pip install --upgrade pip setuptools
- c:\> c:\Python33\Scripts\virtualenv %VENV%
Install Pyramid into the virtual Python environment
---------------------------------------------------
@@ -77,16 +97,17 @@ Install Pyramid into the virtual Python environment
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/easy_install pyramid
+ $ $VENV/bin/pip install pyramid
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
+
+ c:\> %VENV%\Scripts\pip install pyramid
- c:\> %VENV%\Scripts\easy_install pyramid
Install SQLite3 and its development packages
--------------------------------------------
@@ -94,276 +115,334 @@ Install SQLite3 and its development packages
If you used a package manager to install your Python or if you compiled
your Python from source, then you must install SQLite3 and its
development packages. If you downloaded your Python as an installer
-from python.org, then you already have it installed and can proceed to
-the next section :ref:`sql_making_a_project`..
+from https://www.python.org, then you already have it installed and can skip
+this step.
If you need to install the SQLite3 packages, then, for example, using
-the Debian system and apt-get, the command would be the following:
+the Debian system and ``apt-get``, the command would be the following:
-.. code-block:: text
+.. code-block:: bash
$ sudo apt-get install libsqlite3-dev
+
Change directory to your virtual Python environment
---------------------------------------------------
-Change directory to the ``pyramidtut`` directory.
+Change directory to the ``pyramidtut`` directory, which is both your workspace
+and your virtual environment.
On UNIX
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ cd pyramidtut
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\> cd pyramidtut
+
.. _sql_making_a_project:
Making a project
-================
+----------------
Your next step is to create a project. For this tutorial we will use
the :term:`scaffold` named ``alchemy`` which generates an application
that uses :term:`SQLAlchemy` and :term:`URL dispatch`.
-:app:`Pyramid` supplies a variety of scaffolds to generate sample
-projects. We will use `pcreate`—a script that comes with Pyramid to
-quickly and easily generate scaffolds, usually with a single command—to
-create the scaffold for our project.
+:app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We
+will use ``pcreate``, a script that comes with Pyramid, to create our project
+using a scaffold.
-By passing `alchemy` into the `pcreate` command, the script creates
-the files needed to use SQLAlchemy. By passing in our application name
-`tutorial`, the script inserts that application name into all the
-required files. For example, `pcreate` creates the
-``initialize_tutorial_db`` in the ``pyramidtut/bin`` directory.
+By passing ``alchemy`` into the ``pcreate`` command, the script creates the
+files needed to use SQLAlchemy. By passing in our application name
+``tutorial``, the script inserts that application name into all the required
+files. For example, ``pcreate`` creates the ``initialize_tutorial_db`` in the
+``pyramidtut/bin`` directory.
The below instructions assume your current working directory is "pyramidtut".
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/pcreate -s alchemy tutorial
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial
-.. note:: If you are using Windows, the ``alchemy``
- scaffold may not deal gracefully with installation into a
- location that contains spaces in the path. If you experience
- startup problems, try putting both the virtualenv and the project
- into directories that do not contain spaces in their paths.
+.. note:: If you are using Windows, the ``alchemy`` scaffold may not deal
+ gracefully with installation into a location that contains spaces in the
+ path. If you experience startup problems, try putting both the virtual
+ environment and the project into directories that do not contain spaces in
+ their paths.
+
.. _installing_project_in_dev_mode:
Installing the project in development mode
-==========================================
+------------------------------------------
-In order to do development on the project easily, you must "register"
-the project as a development egg in your workspace using the
-``setup.py develop`` command. In order to do so, cd to the `tutorial`
-directory you created in :ref:`sql_making_a_project`, and run the
-``setup.py develop`` command using the virtualenv Python interpreter.
+In order to do development on the project easily, you must "register" the
+project as a development egg in your workspace using the ``pip install -e .``
+command. In order to do so, change directory to the ``tutorial`` directory that
+you created in :ref:`sql_making_a_project`, and run the ``pip install -e .``
+command using the virtual environment Python interpreter.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ cd tutorial
- $ $VENV/bin/python setup.py develop
+ $ $VENV/bin/pip install -e .
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut> cd tutorial
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+ c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e .
+
+The console will show ``pip`` checking for packages and installing missing
+packages. Success executing this command will show a line like the following:
+
+.. code-block:: bash
+
+ Successfully installed Chameleon-2.24 Mako-1.0.4 MarkupSafe-0.23 \
+ Pygments-2.1.3 SQLAlchemy-1.0.12 pyramid-chameleon-0.3 \
+ pyramid-debugtoolbar-2.4.2 pyramid-mako-1.0.2 pyramid-tm-0.12.1 \
+ transaction-1.4.4 tutorial waitress-0.8.10 zope.sqlalchemy-0.7.6
+
+
+.. _install-testing-requirements:
+
+Install testing requirements
+----------------------------
+
+In order to run tests, we need to install the testing requirements. This is
+done through our project's ``setup.py`` file, in the ``tests_require`` and
+``extras_require`` stanzas, and by issuing the command below for your
+operating system.
+
+.. literalinclude:: src/installation/setup.py
+ :language: python
+ :linenos:
+ :lineno-start: 22
+ :lines: 22-26
+
+.. literalinclude:: src/installation/setup.py
+ :language: python
+ :linenos:
+ :lineno-start: 45
+ :lines: 45-47
+
+On UNIX
+^^^^^^^
-The console will show `setup.py` checking for packages and installing
-missing packages. Success executing this command will show a line like
-the following::
+.. code-block:: bash
+
+ $ $VENV/bin/pip install -e ".[testing]"
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e ".[testing]"
- Finished processing dependencies for tutorial==0.0
.. _sql_running_tests:
Run the tests
-=============
+-------------
-After you've installed the project in development mode, you may run
-the tests for the project.
+After you've installed the project in development mode as well as the testing
+requirements, you may run the tests for the project.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/python setup.py test -q
+ $ $VENV/bin/py.test tutorial/tests.py -q
On Windows
-----------
+^^^^^^^^^^
+
+.. code-block:: doscon
-.. code-block:: text
+ c:\pyramidtut\tutorial> %VENV%\Scripts\py.test tutorial\tests.py -q
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
+For a successful test run, you should see output that ends like this:
-For a successful test run, you should see output that ends like this::
+.. code-block:: bash
+
+ ..
+ 2 passed in 0.44 seconds
- .
- ----------------------------------------------------------------------
- Ran 1 test in 0.094s
-
- OK
Expose test coverage information
-================================
+--------------------------------
-You can run the ``nosetests`` command to see test coverage
-information. This runs the tests in the same way that ``setup.py
-test`` does but provides additional "coverage" information, exposing
-which lines of your project are "covered" (or not covered) by the
+You can run the ``py.test`` command to see test coverage information. This
+runs the tests in the same way that ``py.test`` does, but provides additional
+"coverage" information, exposing which lines of your project are covered by the
tests.
-To get this functionality working, we'll need to install the ``nose`` and
-``coverage`` packages into our ``virtualenv``:
+We've already installed the ``pytest-cov`` package into our virtual
+environment, so we can run the tests with coverage.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/easy_install nose coverage
+ $ $VENV/bin/py.test --cov=tutorial --cov-report=term-missing tutorial/tests.py
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\easy_install nose coverage
+ c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial \
+ --cov-report=term-missing tutorial\tests.py
-Once ``nose`` and ``coverage`` are installed, we can actually run the
-coverage tests.
+If successful, you will see output something like this:
-On UNIX
--------
+.. code-block:: bash
-.. code-block:: text
+ ======================== test session starts ========================
+ platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile:
+ plugins: cov-2.2.1
+ collected 2 items
- $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage
+ tutorial/tests.py ..
+ ------------------ coverage: platform Python 3.5.1 ------------------
+ Name Stmts Miss Cover Missing
+ ----------------------------------------------------------------
+ tutorial/__init__.py 8 6 25% 7-12
+ tutorial/models/__init__.py 22 0 100%
+ tutorial/models/meta.py 5 0 100%
+ tutorial/models/mymodel.py 8 0 100%
+ tutorial/routes.py 3 3 0% 1-3
+ tutorial/scripts/__init__.py 0 0 100%
+ tutorial/scripts/initializedb.py 26 26 0% 1-45
+ tutorial/tests.py 39 0 100%
+ tutorial/views/__init__.py 0 0 100%
+ tutorial/views/default.py 12 0 100%
+ tutorial/views/notfound.py 4 4 0% 1-7
+ ----------------------------------------------------------------
+ TOTAL 127 39 69%
-On Windows
-----------
+ ===================== 2 passed in 0.57 seconds ======================
-.. code-block:: text
+Our package doesn't quite have 100% test coverage.
- c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \
- --cover-erase --with-coverage
-If successful, you will see output something like this::
+.. _initialize_db_wiki2:
- .
- Name Stmts Miss Cover Missing
- ---------------------------------------------------
- tutorial.py 13 9 31% 13-21
- tutorial/models.py 12 0 100%
- tutorial/scripts.py 0 0 100%
- tutorial/views.py 11 0 100%
- ---------------------------------------------------
- TOTAL 36 9 75%
- ----------------------------------------------------------------------
- Ran 2 tests in 0.643s
+Initializing the database
+-------------------------
- OK
+We need to use the ``initialize_tutorial_db`` :term:`console script` to
+initialize our database.
-Looks like our package doesn't quite have 100% test coverage.
+.. note::
-.. _initialize_db_wiki2:
+ The ``initialize_tutorial_db`` command does not perform a migration, but
+ rather it simply creates missing tables and adds some dummy data. If you
+ already have a database, you should delete it before running
+ ``initialize_tutorial_db`` again.
-Initializing the database
-=========================
+.. note::
-We need to use the ``initialize_tutorial_db`` :term:`console
-script` to initialize our database.
+ The ``initialize_tutorial_db`` command is not performing a migration but
+ rather simply creating missing tables and adding some dummy data. If you
+ already have a database, you should delete it before running
+ ``initialize_tutorial_db`` again.
Type the following command, making sure you are still in the ``tutorial``
directory (the directory with a ``development.ini`` in it):
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/initialize_tutorial_db development.ini
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini
-The output to your console should be something like this::
-
- 2015-05-23 16:49:49,609 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
- 2015-05-23 16:49:49,609 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
- 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models")
- 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-23 16:49:49,612 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
- CREATE TABLE models (
- id INTEGER NOT NULL,
- name TEXT,
- value INTEGER,
- PRIMARY KEY (id)
- )
-
-
- 2015-05-23 16:49:49,612 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
- 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name)
- 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-23 16:49:49,614 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
- 2015-05-23 16:49:49,616 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
- 2015-05-23 16:49:49,617 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?)
- 2015-05-23 16:49:49,617 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1)
- 2015-05-23 16:49:49,618 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
-
-Success! You should now have a ``tutorial.sqlite`` file in your current working
-directory. This will be a SQLite database with a single table defined in it
+The output to your console should be something like this:
+
+.. code-block:: bash
+
+ 2016-04-09 00:53:37,801 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2016-04-09 00:53:37,801 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models")
+ 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
+ CREATE TABLE models (
+ id INTEGER NOT NULL,
+ name TEXT,
+ value INTEGER,
+ CONSTRAINT pk_models PRIMARY KEY (id)
+ )
+
+
+ 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-04-09 00:53:37,804 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-04-09 00:53:37,805 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name)
+ 2016-04-09 00:53:37,805 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-04-09 00:53:37,806 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-04-09 00:53:37,807 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
+ 2016-04-09 00:53:37,808 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?)
+ 2016-04-09 00:53:37,808 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1)
+ 2016-04-09 00:53:37,809 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+
+Success! You should now have a ``tutorial.sqlite`` file in your current
+working directory. This is an SQLite database with a single table defined in it
(``models``).
.. _wiki2-start-the-application:
Start the application
-=====================
+---------------------
Start the application.
On UNIX
--------
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/pserve development.ini --reload
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload
@@ -374,40 +453,69 @@ On Windows
If successful, you will see something like this on your console::
- Starting subprocess with file monitor
- Starting server in PID 8966.
- Starting HTTP server on http://0.0.0.0:6543
+ Starting subprocess with file monitor
+ Starting server in PID 82349.
+ serving on http://127.0.0.1:6543
This means the server is ready to accept requests.
+
Visit the application in a browser
-==================================
+----------------------------------
-In a browser, visit `http://localhost:6543/ <http://localhost:6543>`_. You
-will see the generated application's default page.
+In a browser, visit http://localhost:6543/. You will see the generated
+application's default page.
One thing you'll notice is the "debug toolbar" icon on right hand side of the
page. You can read more about the purpose of the icon at
:ref:`debug_toolbar`. It allows you to get information about your
application while you develop.
+
Decisions the ``alchemy`` scaffold has made for you
-===================================================
+---------------------------------------------------
Creating a project using the ``alchemy`` scaffold makes the following
assumptions:
-- you are willing to use :term:`SQLAlchemy` as a database access tool
+- You are willing to use :term:`SQLAlchemy` as a database access tool.
-- you are willing to use :term:`URL dispatch` to map URLs to code
+- You are willing to use :term:`URL dispatch` to map URLs to code.
-- you want to use ``ZopeTransactionExtension`` and ``pyramid_tm`` to scope
- sessions to requests
+- You want to use zope.sqlalchemy_, pyramid_tm_, and the transaction_ packages
+ to scope sessions to requests.
+
+- You want to use pyramid_jinja2_ to render your templates. Different
+ templating engines can be used, but we had to choose one to make this
+ tutorial. See :ref:`available_template_system_bindings` for some options.
.. note::
:app:`Pyramid` supports any persistent storage mechanism (e.g., object
- database or filesystem files). It also supports an additional
- mechanism to map URLs to code (:term:`traversal`). However, for the
- purposes of this tutorial, we'll only be using URL dispatch and
- SQLAlchemy.
+ database or filesystem files). It also supports an additional mechanism to
+ map URLs to code (:term:`traversal`). However, for the purposes of this
+ tutorial, we'll only be using URL dispatch and SQLAlchemy.
+
+.. _pyramid_jinja2:
+ http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/
+
+.. _pyramid_tm:
+ http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/
+
+.. _zope.sqlalchemy:
+ https://pypi.python.org/pypi/zope.sqlalchemy
+
+.. _transaction:
+ http://zodb.readthedocs.org/en/latest/transactions.html
+
+.. _pyramid_jinja2:
+ http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/
+
+.. _pyramid_tm:
+ http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/
+
+.. _zope.sqlalchemy:
+ https://pypi.python.org/pypi/zope.sqlalchemy
+
+.. _transaction:
+ http://zodb.readthedocs.org/en/latest/transactions.html
diff --git a/docs/tutorials/wiki2/src/authentication/CHANGES.txt b/docs/tutorials/wiki2/src/authentication/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki2/src/authentication/MANIFEST.in b/docs/tutorials/wiki2/src/authentication/MANIFEST.in
new file mode 100644
index 000000000..42cd299b5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt
new file mode 100644
index 000000000..5b0101e5f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/README.txt
@@ -0,0 +1,14 @@
+tutorial README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/initialize_tutorial_db development.ini
+
+- $VENV/bin/pserve development.ini
+
diff --git a/docs/tutorials/wiki2/src/authentication/development.ini b/docs/tutorials/wiki2/src/authentication/development.ini
new file mode 100644
index 000000000..4a6c9325c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/development.ini
@@ -0,0 +1,73 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+ pyramid_tm
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+auth.secret = seekrit
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+host = 127.0.0.1
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_tutorial]
+level = DEBUG
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = INFO
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authentication/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini
new file mode 100644
index 000000000..a13a0ca19
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/production.ini
@@ -0,0 +1,62 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+auth.secret = real-seekrit
+
+[server:main]
+use = egg:waitress#main
+host = 0.0.0.0
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_tutorial]
+level = WARN
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py
new file mode 100644
index 000000000..def3ce1f6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/setup.py
@@ -0,0 +1,57 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.txt')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'bcrypt',
+ 'docutils',
+ 'pyramid',
+ 'pyramid_jinja2',
+ 'pyramid_debugtoolbar',
+ 'pyramid_tm',
+ 'SQLAlchemy',
+ 'transaction',
+ 'zope.sqlalchemy',
+ 'waitress',
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg pylons pyramid',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
+ install_requires=requires,
+ entry_points="""\
+ [paste.app_factory]
+ main = tutorial:main
+ [console_scripts]
+ initialize_tutorial_db = tutorial.scripts.initializedb:main
+ """,
+ )
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py
new file mode 100644
index 000000000..f5c033b8b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py
@@ -0,0 +1,13 @@
+from pyramid.config import Configurator
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.include('.security')
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/routes.py b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py
new file mode 100644
index 000000000..cb747244f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py
@@ -0,0 +1,8 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('login', '/login')
+ config.add_route('logout', '/logout')
+ config.add_route('view_page', '/{pagename}')
+ config.add_route('add_page', '/add_page/{pagename}')
+ config.add_route('edit_page', '/{pagename}/edit_page')
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py
new file mode 100644
index 000000000..5bb534f79
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py
new file mode 100644
index 000000000..f3c0a6fef
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py
@@ -0,0 +1,57 @@
+import os
+import sys
+import transaction
+
+from pyramid.paster import (
+ get_appsettings,
+ setup_logging,
+ )
+
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+from ..models import Page, User
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri> [var=value]\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) < 2:
+ usage(argv)
+ config_uri = argv[1]
+ options = parse_vars(argv[2:])
+ setup_logging(config_uri)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
+ Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py
new file mode 100644
index 000000000..8ea3858d2
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py
@@ -0,0 +1,27 @@
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+
+from .models import User
+
+
+class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ user = request.user
+ if user is not None:
+ return user.id
+
+def get_user(request):
+ user_id = request.unauthenticated_userid
+ if user_id is not None:
+ user = request.dbsession.query(User).get(user_id)
+ return user
+
+def includeme(config):
+ settings = config.get_settings()
+ authn_policy = MyAuthenticationPolicy(
+ settings['auth.secret'],
+ hashalg='sha512',
+ )
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(ACLAuthorizationPolicy())
+ config.add_request_method(get_user, 'user', reify=True)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png
new file mode 100644
index 000000000..4ab837be9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css
@@ -0,0 +1,154 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+body {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #ffffff;
+ background: #bc2131;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+}
+p {
+ font-weight: 300;
+}
+.font-normal {
+ font-weight: 400;
+}
+.font-semi-bold {
+ font-weight: 600;
+}
+.font-bold {
+ font-weight: 700;
+}
+.starter-template {
+ margin-top: 250px;
+}
+.starter-template .content {
+ margin-left: 10px;
+}
+.starter-template .content h1 {
+ margin-top: 10px;
+ font-size: 60px;
+}
+.starter-template .content h1 .smaller {
+ font-size: 40px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead {
+ font-size: 25px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead .font-normal {
+ color: #ffffff;
+}
+.starter-template .links {
+ float: right;
+ right: 0;
+ margin-top: 125px;
+}
+.starter-template .links ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+}
+.starter-template .links ul li {
+ list-style: none;
+ display: inline;
+ margin: 0 10px;
+}
+.starter-template .links ul li:first-child {
+ margin-left: 0;
+}
+.starter-template .links ul li:last-child {
+ margin-right: 0;
+}
+.starter-template .links ul li.current-version {
+ color: #f2b7bd;
+ font-weight: 400;
+}
+.starter-template .links ul li a, a {
+ color: #f2b7bd;
+ text-decoration: underline;
+}
+.starter-template .links ul li a:hover, a:hover {
+ color: #ffffff;
+ text-decoration: underline;
+}
+.starter-template .links ul li .icon-muted {
+ color: #eb8b95;
+ margin-right: 5px;
+}
+.starter-template .links ul li:hover .icon-muted {
+ color: #ffffff;
+}
+.starter-template .copyright {
+ margin-top: 10px;
+ font-size: 0.9em;
+ color: #f2b7bd;
+ text-transform: lowercase;
+ float: right;
+ right: 0;
+}
+@media (max-width: 1199px) {
+ .starter-template .content h1 {
+ font-size: 45px;
+ }
+ .starter-template .content h1 .smaller {
+ font-size: 30px;
+ }
+ .starter-template .content .lead {
+ font-size: 20px;
+ }
+}
+@media (max-width: 991px) {
+ .starter-template {
+ margin-top: 0;
+ }
+ .starter-template .logo {
+ margin: 40px auto;
+ }
+ .starter-template .content {
+ margin-left: 0;
+ text-align: center;
+ }
+ .starter-template .content h1 {
+ margin-bottom: 20px;
+ }
+ .starter-template .links {
+ float: none;
+ text-align: center;
+ margin-top: 60px;
+ }
+ .starter-template .copyright {
+ float: none;
+ text-align: center;
+ }
+}
+@media (max-width: 767px) {
+ .starter-template .content h1 .smaller {
+ font-size: 25px;
+ display: block;
+ }
+ .starter-template .content .lead {
+ font-size: 16px;
+ }
+ .starter-template .links {
+ margin-top: 40px;
+ }
+ .starter-template .links ul li {
+ display: block;
+ margin: 0;
+ }
+ .starter-template .links ul li .icon-muted {
+ display: none;
+ }
+ .starter-template .copyright {
+ margin-top: 20px;
+ }
+}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
index 0f564b16c..44d14304e 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
@@ -1,21 +1,20 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -23,31 +22,27 @@
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
+
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong><span tal:replace="page.name">
- Page Name Goes Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2
new file mode 100644
index 000000000..1806de0ff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2
@@ -0,0 +1,26 @@
+{% extends 'layout.jinja2' %}
+
+{% block title %}Login - {% endblock title %}
+
+{% block content %}
+<p>
+<strong>
+ Login
+</strong><br>
+{{ message }}
+</p>
+<form action="{{ url }}" method="post">
+<input type="hidden" name="next" value="{{ next_url }}">
+<div class="form-group">
+ <label for="login">Username</label>
+ <input type="text" name="login" value="{{ login }}">
+</div>
+<div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" name="password">
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/tests.py b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py
new file mode 100644
index 000000000..99e95efd3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py
@@ -0,0 +1,65 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
+
+ def tearDown(self):
+ from .models.meta import Base
+
+ testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
+
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py
new file mode 100644
index 000000000..2b993b430
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py
@@ -0,0 +1,46 @@
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import (
+ remember,
+ forget,
+ )
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..models import User
+
+
+@view_config(route_name='login', renderer='../templates/login.jinja2')
+def login(request):
+ next_url = request.params.get('next', request.referrer)
+ if not next_url:
+ next_url = request.route_url('view_wiki')
+ message = ''
+ login = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ user = request.dbsession.query(User).filter_by(name=login).first()
+ if user is not None and user.check_password(password):
+ headers = remember(request, user.id)
+ return HTTPFound(location=next_url, headers=headers)
+ message = 'Failed login'
+
+ return dict(
+ message=message,
+ url=request.route_url('login'),
+ next_url=next_url,
+ login=login,
+ )
+
+@view_config(route_name='logout')
+def logout(request):
+ headers = forget(request)
+ next_url = request.route_url('view_wiki')
+ return HTTPFound(location=next_url, headers=headers)
+
+@forbidden_view_config()
+def forbidden_view(request):
+ next_url = request.route_url('login', _query={'next': request.url})
+ return HTTPFound(location=next_url)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
new file mode 100644
index 000000000..1b071434c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
@@ -0,0 +1,79 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import (
+ HTTPForbidden,
+ HTTPFound,
+ HTTPNotFound,
+ )
+
+from pyramid.view import view_config
+
+from ..models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2')
+def view_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound('No such page')
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=page.name)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2')
+def edit_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ user = request.user
+ if user is None or (user.role != 'editor' and page.creator != user):
+ raise HTTPForbidden
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2')
+def add_page(request):
+ user = request.user
+ if user is None or user.role not in ('editor', 'basic'):
+ raise HTTPForbidden
+ pagename = request.matchdict['pagename']
+ if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ next_url = request.route_url('edit_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = request.user
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/authorization/MANIFEST.in b/docs/tutorials/wiki2/src/authorization/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/authorization/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/authorization/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt
index 68f430110..5b0101e5f 100644
--- a/docs/tutorials/wiki2/src/authorization/README.txt
+++ b/docs/tutorials/wiki2/src/authorization/README.txt
@@ -6,7 +6,7 @@ Getting Started
- cd <directory containing this file>
-- $VENV/bin/python setup.py develop
+- $VENV/bin/pip install -e .
- $VENV/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini
index a9d53b296..4a6c9325c 100644
--- a/docs/tutorials/wiki2/src/authorization/development.ini
+++ b/docs/tutorials/wiki2/src/authorization/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -17,6 +17,8 @@ pyramid.includes =
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+auth.secret = seekrit
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
@@ -27,12 +29,12 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -68,4 +70,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini
index 4684d2f7a..a13a0ca19 100644
--- a/docs/tutorials/wiki2/src/authorization/production.ini
+++ b/docs/tutorials/wiki2/src/authorization/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,17 +11,20 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+auth.secret = real-seekrit
+
[server:main]
use = egg:waitress#main
host = 0.0.0.0
port = 6543
-# Begin logging configuration
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +59,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
-
-# End logging configuration
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py
index 09bd63d33..def3ce1f6 100644
--- a/docs/tutorials/wiki2/src/authorization/setup.py
+++ b/docs/tutorials/wiki2/src/authorization/setup.py
@@ -9,15 +9,22 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
]
setup(name='tutorial',
@@ -25,11 +32,11 @@ setup(name='tutorial',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -37,7 +44,9 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- test_suite='tutorial',
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
index 2ada42171..f5c033b8b 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
@@ -1,37 +1,13 @@
from pyramid.config import Configurator
-from pyramid.authentication import AuthTktAuthenticationPolicy
-from pyramid.authorization import ACLAuthorizationPolicy
-
-from sqlalchemy import engine_from_config
-
-from tutorial.security import groupfinder
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
- authn_policy = AuthTktAuthenticationPolicy(
- 'sosecret', callback=groupfinder, hashalg='sha512')
- authz_policy = ACLAuthorizationPolicy()
- config = Configurator(settings=settings,
- root_factory='tutorial.models.RootFactory')
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(authz_policy)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('view_wiki', '/')
- config.add_route('login', '/login')
- config.add_route('logout', '/logout')
- config.add_route('view_page', '/{pagename}')
- config.add_route('add_page', '/add_page/{pagename}')
- config.add_route('edit_page', '/{pagename}/edit_page')
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.include('.security')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models.py b/docs/tutorials/wiki2/src/authorization/tutorial/models.py
deleted file mode 100644
index 4f7e1e024..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from pyramid.security import (
- Allow,
- Everyone,
- )
-
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- sessionmaker,
- )
-
-from zope.sqlalchemy import ZopeTransactionExtension
-
-DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
-Base = declarative_base()
-
-
-class Page(Base):
- """ The SQLAlchemy declarative model class for a Page object. """
- __tablename__ = 'pages'
- id = Column(Integer, primary_key=True)
- name = Column(Text, unique=True)
- data = Column(Text)
-
-
-class RootFactory(object):
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
- def __init__(self, request):
- pass
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
new file mode 100644
index 000000000..f0a8b7f96
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
@@ -0,0 +1,56 @@
+from pyramid.httpexceptions import (
+ HTTPNotFound,
+ HTTPFound,
+)
+from pyramid.security import (
+ Allow,
+ Everyone,
+)
+
+from .models import Page
+
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('login', '/login')
+ config.add_route('logout', '/logout')
+ config.add_route('view_page', '/{pagename}', factory=page_factory)
+ config.add_route('add_page', '/add_page/{pagename}',
+ factory=new_page_factory)
+ config.add_route('edit_page', '/{pagename}/edit_page',
+ factory=page_factory)
+
+def new_page_factory(request):
+ pagename = request.matchdict['pagename']
+ if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ next_url = request.route_url('edit_page', pagename=pagename)
+ raise HTTPFound(location=next_url)
+ return NewPage(pagename)
+
+class NewPage(object):
+ def __init__(self, pagename):
+ self.pagename = pagename
+
+ def __acl__(self):
+ return [
+ (Allow, 'role:editor', 'create'),
+ (Allow, 'role:basic', 'create'),
+ ]
+
+def page_factory(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound
+ return PageResource(page)
+
+class PageResource(object):
+ def __init__(self, page):
+ self.page = page
+
+ def __acl__(self):
+ return [
+ (Allow, Everyone, 'view'),
+ (Allow, 'role:editor', 'edit'),
+ (Allow, str(self.page.creator_id), 'edit'),
+ ]
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
index 23a5f13f4..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
@@ -2,36 +2,56 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- Page,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import Page, User
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
index d88c9c71f..25cff7b05 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
@@ -1,7 +1,40 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
-GROUPS = {'editor':['group:editors']}
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+)
-def groupfinder(userid, request):
- if userid in USERS:
- return GROUPS.get(userid, [])
+from .models import User
+
+
+class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ user = request.user
+ if user is not None:
+ return user.id
+
+ def effective_principals(self, request):
+ principals = [Everyone]
+ user = request.user
+ if user is not None:
+ principals.append(Authenticated)
+ principals.append(str(user.id))
+ principals.append('role:' + user.role)
+ return principals
+
+def get_user(request):
+ user_id = request.unauthenticated_userid
+ if user_id is not None:
+ user = request.dbsession.query(User).get(user_id)
+ return user
+
+def includeme(config):
+ settings = config.get_settings()
+ authn_policy = MyAuthenticationPolicy(
+ settings['auth.secret'],
+ hashalg='sha512',
+ )
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(ACLAuthorizationPolicy())
+ config.add_request_method(get_user, 'user', reify=True)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt
deleted file mode 100644
index ed355434d..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <p tal:condition="logged_in" class="pull-right">
- <a href="${request.application_url}/logout">Logout</a>
- </p>
- <p>
- Editing <strong><span tal:replace="page.name">Page Name Goes
- Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
- <form action="${save_url}" method="post">
- <div class="form-group">
- <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea>
- </div>
- <div class="form-group">
- <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
- </div>
- </form>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
index 02cb8e73b..44d14304e 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
@@ -1,21 +1,20 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -23,34 +22,27 @@
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
+
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
- <p tal:condition="logged_in" class="pull-right">
- <a href="${request.application_url}/logout">Logout</a>
- </p>
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong><span tal:replace="page.name">
- Page Name Goes Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
new file mode 100644
index 000000000..1806de0ff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
@@ -0,0 +1,26 @@
+{% extends 'layout.jinja2' %}
+
+{% block title %}Login - {% endblock title %}
+
+{% block content %}
+<p>
+<strong>
+ Login
+</strong><br>
+{{ message }}
+</p>
+<form action="{{ url }}" method="post">
+<input type="hidden" name="next" value="{{ next_url }}">
+<div class="form-group">
+ <label for="login">Username</label>
+ <input type="text" name="login" value="{{ login }}">
+</div>
+<div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" name="password">
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt
deleted file mode 100644
index 4a938e9bb..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt
+++ /dev/null
@@ -1,74 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>Login - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <p>
- <strong>
- Login
- </strong><br>
- <span tal:replace="message"></span>
- </p>
- <form action="${url}" method="post">
- <input type="hidden" name="came_from" value="${came_from}">
- <div class="form-group">
- <label for="login">Username</label>
- <input type="text" name="login" value="${login}">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" name="password" value="${password}">
- </div>
- <div class="form-group">
- <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
- </div>
- </form>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
index 9f01d2da5..99e95efd3 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
@@ -3,144 +3,63 @@ import transaction
from pyramid import testing
-def _initTestingDB():
- from sqlalchemy import create_engine
- from tutorial.models import (
- DBSession,
- Page,
- Base
- )
- engine = create_engine('sqlite://')
- Base.metadata.create_all(engine)
- DBSession.configure(bind=engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
- return DBSession
-
-def _registerRoutes(config):
- config.add_route('view_page', '{pagename}')
- config.add_route('edit_page', '{pagename}/edit_page')
- config.add_route('add_page', 'add_page/{pagename}')
-
-class ViewWikiTests(unittest.TestCase):
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- self.session = _initTestingDB()
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
- def _callFUT(self, request):
- from tutorial.views import view_wiki
- return view_wiki(request)
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
- def test_it(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/FrontPage')
+ self.session = get_tm_session(session_factory, transaction.manager)
-class ViewPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- def _callFUT(self, request):
- from tutorial.views import view_page
- return view_page(request)
-
- def test_it(self):
- from tutorial.models import Page
- request = testing.DummyRequest()
- request.matchdict['pagename'] = 'IDoExist'
- page = Page(name='IDoExist', data='Hello CruelWorld IDoExist')
- self.session.add(page)
- _registerRoutes(self.config)
- info = self._callFUT(request)
- self.assertEqual(info['page'], page)
- self.assertEqual(
- info['content'],
- '<div class="document">\n'
- '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
- 'CruelWorld</a> '
- '<a href="http://example.com/IDoExist">'
- 'IDoExist</a>'
- '</p>\n</div>\n')
- self.assertEqual(info['edit_url'],
- 'http://example.com/IDoExist/edit_page')
-
-
-class AddPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
+ from .models.meta import Base
- def tearDown(self):
- self.session.remove()
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
- def _callFUT(self, request):
- from tutorial.views import add_page
- return add_page(request)
-
- def test_it_notsubmitted(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- request.matchdict = {'pagename':'AnotherPage'}
- info = self._callFUT(request)
- self.assertEqual(info['page'].data,'')
- self.assertEqual(info['save_url'],
- 'http://example.com/add_page/AnotherPage')
-
- def test_it_submitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- request.matchdict = {'pagename':'AnotherPage'}
- self._callFUT(request)
- page = self.session.query(Page).filter_by(name='AnotherPage').one()
- self.assertEqual(page.data, 'Hello yo!')
-
-class EditPageTests(unittest.TestCase):
def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
- def _callFUT(self, request):
- from tutorial.views import edit_page
- return edit_page(request)
-
- def test_it_notsubmitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- request.matchdict = {'pagename':'abc'}
- page = Page(name='abc', data='hello')
- self.session.add(page)
- info = self._callFUT(request)
- self.assertEqual(info['page'], page)
- self.assertEqual(info['save_url'],
- 'http://example.com/abc/edit_page')
-
- def test_it_submitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- request.matchdict = {'pagename':'abc'}
- page = Page(name='abc', data='hello')
- self.session.add(page)
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/abc')
- self.assertEqual(page.data, 'Hello yo!')
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
deleted file mode 100644
index e954d5a31..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import re
-from docutils.core import publish_parts
-
-from pyramid.httpexceptions import (
- HTTPFound,
- HTTPNotFound,
- )
-
-from pyramid.view import (
- view_config,
- forbidden_view_config,
- )
-
-from pyramid.security import (
- remember,
- forget,
- )
-
-from .security import USERS
-
-from .models import (
- DBSession,
- Page,
- )
-
-
-# regular expression used to find WikiWords
-wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-
-@view_config(route_name='view_wiki',
- permission='view')
-def view_wiki(request):
- return HTTPFound(location = request.route_url('view_page',
- pagename='FrontPage'))
-
-@view_config(route_name='view_page', renderer='templates/view.pt',
- permission='view')
-def view_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).first()
- if page is None:
- return HTTPNotFound('No such page')
-
- def check(match):
- word = match.group(1)
- exists = DBSession.query(Page).filter_by(name=word).all()
- if exists:
- view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, word)
- else:
- add_url = request.route_url('add_page', pagename=word)
- return '<a href="%s">%s</a>' % (add_url, word)
-
- content = publish_parts(page.data, writer_name='html')['html_body']
- content = wikiwords.sub(check, content)
- edit_url = request.route_url('edit_page', pagename=pagename)
- return dict(page=page, content=content, edit_url=edit_url,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='add_page', renderer='templates/edit.pt',
- permission='edit')
-def add_page(request):
- pagename = request.matchdict['pagename']
- if 'form.submitted' in request.params:
- body = request.params['body']
- page = Page(name=pagename, data=body)
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- save_url = request.route_url('add_page', pagename=pagename)
- page = Page(name='', data='')
- return dict(page=page, save_url=save_url,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='edit_page', renderer='templates/edit.pt',
- permission='edit')
-def edit_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).one()
- if 'form.submitted' in request.params:
- page.data = request.params['body']
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- return dict(
- page=page,
- save_url=request.route_url('edit_page', pagename=pagename),
- logged_in=request.authenticated_userid
- )
-
-@view_config(route_name='login', renderer='templates/login.pt')
-@forbidden_view_config(renderer='templates/login.pt')
-def login(request):
- login_url = request.route_url('login')
- referrer = request.url
- if referrer == login_url:
- referrer = '/' # never use the login form itself as came_from
- came_from = request.params.get('came_from', referrer)
- message = ''
- login = ''
- password = ''
- if 'form.submitted' in request.params:
- login = request.params['login']
- password = request.params['password']
- if USERS.get(login) == password:
- headers = remember(request, login)
- return HTTPFound(location = came_from,
- headers = headers)
- message = 'Failed login'
-
- return dict(
- message = message,
- url = request.application_url + '/login',
- came_from = came_from,
- login = login,
- password = password,
- )
-
-@view_config(route_name='logout')
-def logout(request):
- headers = forget(request)
- return HTTPFound(location = request.route_url('view_wiki'),
- headers = headers)
-
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py
new file mode 100644
index 000000000..2b993b430
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py
@@ -0,0 +1,46 @@
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import (
+ remember,
+ forget,
+ )
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..models import User
+
+
+@view_config(route_name='login', renderer='../templates/login.jinja2')
+def login(request):
+ next_url = request.params.get('next', request.referrer)
+ if not next_url:
+ next_url = request.route_url('view_wiki')
+ message = ''
+ login = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ user = request.dbsession.query(User).filter_by(name=login).first()
+ if user is not None and user.check_password(password):
+ headers = remember(request, user.id)
+ return HTTPFound(location=next_url, headers=headers)
+ message = 'Failed login'
+
+ return dict(
+ message=message,
+ url=request.route_url('login'),
+ next_url=next_url,
+ login=login,
+ )
+
+@view_config(route_name='logout')
+def logout(request):
+ headers = forget(request)
+ next_url = request.route_url('view_wiki')
+ return HTTPFound(location=next_url, headers=headers)
+
+@forbidden_view_config()
+def forbidden_view(request):
+ next_url = request.route_url('login', _query={'next': request.url})
+ return HTTPFound(location=next_url)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
new file mode 100644
index 000000000..9358993ea
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
@@ -0,0 +1,64 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+from ..models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2',
+ permission='view')
+def view_page(request):
+ page = request.context.page
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=page.name)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2',
+ permission='edit')
+def edit_page(request):
+ page = request.context.page
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2',
+ permission='create')
+def add_page(request):
+ pagename = request.context.pagename
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = request.user
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt
index 68f430110..5b0101e5f 100644
--- a/docs/tutorials/wiki2/src/basiclayout/README.txt
+++ b/docs/tutorials/wiki2/src/basiclayout/README.txt
@@ -6,7 +6,7 @@ Getting Started
- cd <directory containing this file>
-- $VENV/bin/python setup.py develop
+- $VENV/bin/pip install -e .
- $VENV/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini
index a9d53b296..22b733e10 100644
--- a/docs/tutorials/wiki2/src/basiclayout/development.ini
+++ b/docs/tutorials/wiki2/src/basiclayout/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -27,12 +27,12 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -68,4 +68,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini
index fa94c1b3e..d2ecfe22a 100644
--- a/docs/tutorials/wiki2/src/basiclayout/production.ini
+++ b/docs/tutorials/wiki2/src/basiclayout/production.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -11,8 +11,6 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
@@ -23,7 +21,7 @@ port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -59,4 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py
index 15e7e5923..ede0a82ef 100644
--- a/docs/tutorials/wiki2/src/basiclayout/setup.py
+++ b/docs/tutorials/wiki2/src/basiclayout/setup.py
@@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
requires = [
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
@@ -19,16 +19,22 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -36,7 +42,9 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- test_suite='tutorial',
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
index 867049e4f..4dab44823 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
@@ -1,21 +1,12 @@
from pyramid.config import Configurator
-from sqlalchemy import engine_from_config
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
config = Configurator(settings=settings)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('home', '/')
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py
deleted file mode 100644
index 11ddccadb..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- Index,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- sessionmaker,
- )
-
-from zope.sqlalchemy import ZopeTransactionExtension
-
-DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
-Base = declarative_base()
-
-
-class MyModel(Base):
- __tablename__ = 'models'
- id = Column(Integer, primary_key=True)
- name = Column(Text, unique=True)
- value = Column(Integer)
-
-Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
new file mode 100644
index 000000000..48a957ecb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
@@ -0,0 +1,73 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .mymodel import MyModel # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py
new file mode 100644
index 000000000..d65a01a42
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py
@@ -0,0 +1,18 @@
+from sqlalchemy import (
+ Column,
+ Index,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class MyModel(Base):
+ __tablename__ = 'models'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text)
+ value = Column(Integer)
+
+
+Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py
new file mode 100644
index 000000000..25504ad4d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py
@@ -0,0 +1,3 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
index 66feb3008..7307ecc5c 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
@@ -2,36 +2,44 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- MyModel,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import MyModel
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
model = MyModel(name='one', value=1)
- DBSession.add(model)
+ dbsession.add(model)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..1917f83c7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2
index c9b0cec21..ab8c5ea3d 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2
@@ -1,12 +1,12 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
<title>Alchemy Scaffold for The Pyramid Web Framework</title>
@@ -14,7 +14,7 @@
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -29,19 +29,19 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2
new file mode 100644
index 000000000..6b49869c4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
index 57a775e0a..99e95efd3 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
@@ -3,31 +3,63 @@ import transaction
from pyramid import testing
-from .models import DBSession
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
-class TestMyView(unittest.TestCase):
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- from sqlalchemy import create_engine
- engine = create_engine('sqlite://')
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
from .models import (
- Base,
- MyModel,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = MyModel(name='one', value=55)
- DBSession.add(model)
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- DBSession.remove()
+ from .models.meta import Base
+
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
- def test_it(self):
- from .views import my_view
- request = testing.DummyRequest()
- info = my_view(request)
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
self.assertEqual(info['one'].name, 'one')
self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
index 4cfcae4af..ad0c728d7 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
@@ -3,26 +3,25 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from .models import (
- DBSession,
- MyModel,
- )
+from ..models import MyModel
-@view_config(route_name='home', renderer='templates/mytemplate.pt')
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(MyModel)
+ one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
- return Response(conn_err_msg, content_type='text/plain', status_int=500)
+ return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'tutorial'}
-conn_err_msg = """\
+
+db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
1. You may need to run the "initialize_tutorial_db" script
- to initialize your database tables. Check your virtual
+ to initialize your database tables. Check your virtual
environment's "bin" directory for this script and try to run it.
2. Your database server may not be running. Check that the
@@ -32,4 +31,3 @@ might be caused by one of the following things:
After you fix the problem, please restart the Pyramid application to
try it again.
"""
-
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/installation/CHANGES.txt b/docs/tutorials/wiki2/src/installation/CHANGES.txt
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/docs/tutorials/wiki2/src/installation/MANIFEST.in b/docs/tutorials/wiki2/src/installation/MANIFEST.in
new file mode 100644
index 000000000..42cd299b5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/installation/README.txt b/docs/tutorials/wiki2/src/installation/README.txt
new file mode 100644
index 000000000..5b0101e5f
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/README.txt
@@ -0,0 +1,14 @@
+tutorial README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/initialize_tutorial_db development.ini
+
+- $VENV/bin/pserve development.ini
+
diff --git a/docs/tutorials/wiki2/src/installation/development.ini b/docs/tutorials/wiki2/src/installation/development.ini
new file mode 100644
index 000000000..22b733e10
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/development.ini
@@ -0,0 +1,71 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+ pyramid_tm
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+host = 127.0.0.1
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_tutorial]
+level = DEBUG
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = INFO
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/installation/production.ini b/docs/tutorials/wiki2/src/installation/production.ini
new file mode 100644
index 000000000..d2ecfe22a
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/production.ini
@@ -0,0 +1,60 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+[server:main]
+use = egg:waitress#main
+host = 0.0.0.0
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_tutorial]
+level = WARN
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/installation/setup.py b/docs/tutorials/wiki2/src/installation/setup.py
new file mode 100644
index 000000000..ede0a82ef
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/setup.py
@@ -0,0 +1,55 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.txt')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'pyramid',
+ 'pyramid_jinja2',
+ 'pyramid_debugtoolbar',
+ 'pyramid_tm',
+ 'SQLAlchemy',
+ 'transaction',
+ 'zope.sqlalchemy',
+ 'waitress',
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
+setup(name='tutorial',
+ version='0.0',
+ description='tutorial',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg pylons pyramid',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
+ install_requires=requires,
+ entry_points="""\
+ [paste.app_factory]
+ main = tutorial:main
+ [console_scripts]
+ initialize_tutorial_db = tutorial.scripts.initializedb:main
+ """,
+ )
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/__init__.py
new file mode 100644
index 000000000..4dab44823
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/__init__.py
@@ -0,0 +1,12 @@
+from pyramid.config import Configurator
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
new file mode 100644
index 000000000..48a957ecb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
@@ -0,0 +1,73 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .mymodel import MyModel # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py
new file mode 100644
index 000000000..d65a01a42
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py
@@ -0,0 +1,18 @@
+from sqlalchemy import (
+ Column,
+ Index,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class MyModel(Base):
+ __tablename__ = 'models'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text)
+ value = Column(Integer)
+
+
+Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/routes.py b/docs/tutorials/wiki2/src/installation/tutorial/routes.py
new file mode 100644
index 000000000..25504ad4d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/routes.py
@@ -0,0 +1,3 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py
new file mode 100644
index 000000000..5bb534f79
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py
new file mode 100644
index 000000000..7307ecc5c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py
@@ -0,0 +1,45 @@
+import os
+import sys
+import transaction
+
+from pyramid.paster import (
+ get_appsettings,
+ setup_logging,
+ )
+
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+from ..models import MyModel
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri> [var=value]\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) < 2:
+ usage(argv)
+ config_uri = argv[1]
+ options = parse_vars(argv[2:])
+ setup_logging(config_uri)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
+ Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ model = MyModel(name='one', value=1)
+ dbsession.add(model)
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png
new file mode 100644
index 000000000..4ab837be9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css b/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css
@@ -0,0 +1,154 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+body {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #ffffff;
+ background: #bc2131;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+}
+p {
+ font-weight: 300;
+}
+.font-normal {
+ font-weight: 400;
+}
+.font-semi-bold {
+ font-weight: 600;
+}
+.font-bold {
+ font-weight: 700;
+}
+.starter-template {
+ margin-top: 250px;
+}
+.starter-template .content {
+ margin-left: 10px;
+}
+.starter-template .content h1 {
+ margin-top: 10px;
+ font-size: 60px;
+}
+.starter-template .content h1 .smaller {
+ font-size: 40px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead {
+ font-size: 25px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead .font-normal {
+ color: #ffffff;
+}
+.starter-template .links {
+ float: right;
+ right: 0;
+ margin-top: 125px;
+}
+.starter-template .links ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+}
+.starter-template .links ul li {
+ list-style: none;
+ display: inline;
+ margin: 0 10px;
+}
+.starter-template .links ul li:first-child {
+ margin-left: 0;
+}
+.starter-template .links ul li:last-child {
+ margin-right: 0;
+}
+.starter-template .links ul li.current-version {
+ color: #f2b7bd;
+ font-weight: 400;
+}
+.starter-template .links ul li a, a {
+ color: #f2b7bd;
+ text-decoration: underline;
+}
+.starter-template .links ul li a:hover, a:hover {
+ color: #ffffff;
+ text-decoration: underline;
+}
+.starter-template .links ul li .icon-muted {
+ color: #eb8b95;
+ margin-right: 5px;
+}
+.starter-template .links ul li:hover .icon-muted {
+ color: #ffffff;
+}
+.starter-template .copyright {
+ margin-top: 10px;
+ font-size: 0.9em;
+ color: #f2b7bd;
+ text-transform: lowercase;
+ float: right;
+ right: 0;
+}
+@media (max-width: 1199px) {
+ .starter-template .content h1 {
+ font-size: 45px;
+ }
+ .starter-template .content h1 .smaller {
+ font-size: 30px;
+ }
+ .starter-template .content .lead {
+ font-size: 20px;
+ }
+}
+@media (max-width: 991px) {
+ .starter-template {
+ margin-top: 0;
+ }
+ .starter-template .logo {
+ margin: 40px auto;
+ }
+ .starter-template .content {
+ margin-left: 0;
+ text-align: center;
+ }
+ .starter-template .content h1 {
+ margin-bottom: 20px;
+ }
+ .starter-template .links {
+ float: none;
+ text-align: center;
+ margin-top: 60px;
+ }
+ .starter-template .copyright {
+ float: none;
+ text-align: center;
+ }
+}
+@media (max-width: 767px) {
+ .starter-template .content h1 .smaller {
+ font-size: 25px;
+ display: block;
+ }
+ .starter-template .content .lead {
+ font-size: 16px;
+ }
+ .starter-template .links {
+ margin-top: 40px;
+ }
+ .starter-template .links ul li {
+ display: block;
+ margin: 0;
+ }
+ .starter-template .links ul li .icon-muted {
+ display: none;
+ }
+ .starter-template .copyright {
+ margin-top: 20px;
+ }
+}
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..1917f83c7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2
index c9b0cec21..ab8c5ea3d 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2
@@ -1,12 +1,12 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
<title>Alchemy Scaffold for The Pyramid Web Framework</title>
@@ -14,7 +14,7 @@
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -29,19 +29,19 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
</div>
</div>
<div class="row">
<div class="links">
<ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2
new file mode 100644
index 000000000..6b49869c4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/tests.py b/docs/tutorials/wiki2/src/installation/tutorial/tests.py
new file mode 100644
index 000000000..99e95efd3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/tests.py
@@ -0,0 +1,65 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
+
+ def tearDown(self):
+ from .models.meta import Base
+
+ testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
+
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views.py b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
index 4cfcae4af..ad0c728d7 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
@@ -3,26 +3,25 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from .models import (
- DBSession,
- MyModel,
- )
+from ..models import MyModel
-@view_config(route_name='home', renderer='templates/mytemplate.pt')
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(MyModel)
+ one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
- return Response(conn_err_msg, content_type='text/plain', status_int=500)
+ return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'tutorial'}
-conn_err_msg = """\
+
+db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
1. You may need to run the "initialize_tutorial_db" script
- to initialize your database tables. Check your virtual
+ to initialize your database tables. Check your virtual
environment's "bin" directory for this script and try to run it.
2. Your database server may not be running. Check that the
@@ -32,4 +31,3 @@ might be caused by one of the following things:
After you fix the problem, please restart the Pyramid application to
try it again.
"""
-
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/models/MANIFEST.in b/docs/tutorials/wiki2/src/models/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/models/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/models/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt
index 68f430110..5b0101e5f 100644
--- a/docs/tutorials/wiki2/src/models/README.txt
+++ b/docs/tutorials/wiki2/src/models/README.txt
@@ -6,7 +6,7 @@ Getting Started
- cd <directory containing this file>
-- $VENV/bin/python setup.py develop
+- $VENV/bin/pip install -e .
- $VENV/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini
index a9d53b296..22b733e10 100644
--- a/docs/tutorials/wiki2/src/models/development.ini
+++ b/docs/tutorials/wiki2/src/models/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -27,12 +27,12 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -68,4 +68,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini
index 4684d2f7a..d2ecfe22a 100644
--- a/docs/tutorials/wiki2/src/models/production.ini
+++ b/docs/tutorials/wiki2/src/models/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,8 +11,6 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
@@ -16,7 +19,10 @@ use = egg:waitress#main
host = 0.0.0.0
port = 6543
-# Begin logging configuration
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
-
-# End logging configuration
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py
index 15e7e5923..742a7c59c 100644
--- a/docs/tutorials/wiki2/src/models/setup.py
+++ b/docs/tutorials/wiki2/src/models/setup.py
@@ -9,8 +9,9 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
@@ -19,16 +20,22 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='tutorial',
version='0.0',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -36,7 +43,9 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- test_suite='tutorial',
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
index 867049e4f..4dab44823 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
@@ -1,21 +1,12 @@
from pyramid.config import Configurator
-from sqlalchemy import engine_from_config
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
config = Configurator(settings=settings)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('home', '/')
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models.py b/docs/tutorials/wiki2/src/models/tutorial/models.py
deleted file mode 100644
index f028c917a..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/models.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- sessionmaker,
- )
-
-from zope.sqlalchemy import ZopeTransactionExtension
-
-DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
-Base = declarative_base()
-
-
-class Page(Base):
- """ The SQLAlchemy declarative model class for a Page object. """
- __tablename__ = 'pages'
- id = Column(Integer, primary_key=True)
- name = Column(Text, unique=True)
- data = Column(Text)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/page.py b/docs/tutorials/wiki2/src/models/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/models/tutorial/routes.py b/docs/tutorials/wiki2/src/models/tutorial/routes.py
new file mode 100644
index 000000000..25504ad4d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/routes.py
@@ -0,0 +1,3 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
index 23a5f13f4..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
@@ -2,36 +2,56 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- Page,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import Page, User
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..1917f83c7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..ab8c5ea3d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="{{request.locale_name}}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>Alchemy Scaffold for The Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!--[if lt IE 9]>
+ <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
+ <![endif]-->
+ </head>
+
+ <body>
+
+ <div class="starter-template">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-2">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
+ </div>
+ <div class="col-md-10">
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
+ </div>
+ </div>
+ <div class="row">
+ <div class="links">
+ <ul>
+ <li class="current-version">Generated by v1.7</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li>
+ <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
+ <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
+ <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
+ </body>
+</html>
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2
new file mode 100644
index 000000000..6b49869c4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py
index 57a775e0a..99e95efd3 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py
@@ -3,31 +3,63 @@ import transaction
from pyramid import testing
-from .models import DBSession
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
-class TestMyView(unittest.TestCase):
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- from sqlalchemy import create_engine
- engine = create_engine('sqlite://')
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
from .models import (
- Base,
- MyModel,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = MyModel(name='one', value=55)
- DBSession.add(model)
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- DBSession.remove()
+ from .models.meta import Base
+
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
- def test_it(self):
- from .views import my_view
- request = testing.DummyRequest()
- info = my_view(request)
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
self.assertEqual(info['one'].name, 'one')
self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
new file mode 100644
index 000000000..ad0c728d7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
@@ -0,0 +1,33 @@
+from pyramid.response import Response
+from pyramid.view import view_config
+
+from sqlalchemy.exc import DBAPIError
+
+from ..models import MyModel
+
+
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
+def my_view(request):
+ try:
+ query = request.dbsession.query(MyModel)
+ one = query.filter(MyModel.name == 'one').first()
+ except DBAPIError:
+ return Response(db_err_msg, content_type='text/plain', status=500)
+ return {'one': one, 'project': 'tutorial'}
+
+
+db_err_msg = """\
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to run the "initialize_tutorial_db" script
+ to initialize your database tables. Check your virtual
+ environment's "bin" directory for this script and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+
+After you fix the problem, please restart the Pyramid application to
+try it again.
+"""
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/tests/MANIFEST.in b/docs/tutorials/wiki2/src/tests/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/tests/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/tests/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt
index 68f430110..5b0101e5f 100644
--- a/docs/tutorials/wiki2/src/tests/README.txt
+++ b/docs/tutorials/wiki2/src/tests/README.txt
@@ -6,7 +6,7 @@ Getting Started
- cd <directory containing this file>
-- $VENV/bin/python setup.py develop
+- $VENV/bin/pip install -e .
- $VENV/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini
index a9d53b296..4a6c9325c 100644
--- a/docs/tutorials/wiki2/src/tests/development.ini
+++ b/docs/tutorials/wiki2/src/tests/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -17,6 +17,8 @@ pyramid.includes =
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+auth.secret = seekrit
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
@@ -27,12 +29,12 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -68,4 +70,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini
index 4684d2f7a..a13a0ca19 100644
--- a/docs/tutorials/wiki2/src/tests/production.ini
+++ b/docs/tutorials/wiki2/src/tests/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,17 +11,20 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+auth.secret = real-seekrit
+
[server:main]
use = egg:waitress#main
host = 0.0.0.0
port = 6543
-# Begin logging configuration
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +59,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
-
-# End logging configuration
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py
index d8486e462..def3ce1f6 100644
--- a/docs/tutorials/wiki2/src/tests/setup.py
+++ b/docs/tutorials/wiki2/src/tests/setup.py
@@ -9,16 +9,22 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
- 'WebTest', # add this
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
]
setup(name='tutorial',
@@ -26,11 +32,11 @@ setup(name='tutorial',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -38,7 +44,9 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- test_suite='tutorial',
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
index cee89184b..f5c033b8b 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
@@ -1,37 +1,13 @@
from pyramid.config import Configurator
-from pyramid.authentication import AuthTktAuthenticationPolicy
-from pyramid.authorization import ACLAuthorizationPolicy
-
-from sqlalchemy import engine_from_config
-
-from tutorial.security import groupfinder
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
- authn_policy = AuthTktAuthenticationPolicy(
- 'sosecret', callback=groupfinder, hashalg='sha512')
- authz_policy = ACLAuthorizationPolicy()
- config = Configurator(settings=settings,
- root_factory='tutorial.models.RootFactory')
- config.include('pyramid_chameleon')
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(authz_policy)
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('view_wiki', '/')
- config.add_route('login', '/login')
- config.add_route('logout', '/logout')
- config.add_route('view_page', '/{pagename}')
- config.add_route('add_page', '/add_page/{pagename}')
- config.add_route('edit_page', '/{pagename}/edit_page')
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.include('.security')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models.py b/docs/tutorials/wiki2/src/tests/tutorial/models.py
deleted file mode 100644
index 4f7e1e024..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/models.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from pyramid.security import (
- Allow,
- Everyone,
- )
-
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- sessionmaker,
- )
-
-from zope.sqlalchemy import ZopeTransactionExtension
-
-DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
-Base = declarative_base()
-
-
-class Page(Base):
- """ The SQLAlchemy declarative model class for a Page object. """
- __tablename__ = 'pages'
- id = Column(Integer, primary_key=True)
- name = Column(Text, unique=True)
- data = Column(Text)
-
-
-class RootFactory(object):
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
- def __init__(self, request):
- pass
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/page.py b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py
new file mode 100644
index 000000000..f0a8b7f96
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py
@@ -0,0 +1,56 @@
+from pyramid.httpexceptions import (
+ HTTPNotFound,
+ HTTPFound,
+)
+from pyramid.security import (
+ Allow,
+ Everyone,
+)
+
+from .models import Page
+
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('login', '/login')
+ config.add_route('logout', '/logout')
+ config.add_route('view_page', '/{pagename}', factory=page_factory)
+ config.add_route('add_page', '/add_page/{pagename}',
+ factory=new_page_factory)
+ config.add_route('edit_page', '/{pagename}/edit_page',
+ factory=page_factory)
+
+def new_page_factory(request):
+ pagename = request.matchdict['pagename']
+ if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ next_url = request.route_url('edit_page', pagename=pagename)
+ raise HTTPFound(location=next_url)
+ return NewPage(pagename)
+
+class NewPage(object):
+ def __init__(self, pagename):
+ self.pagename = pagename
+
+ def __acl__(self):
+ return [
+ (Allow, 'role:editor', 'create'),
+ (Allow, 'role:basic', 'create'),
+ ]
+
+def page_factory(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound
+ return PageResource(page)
+
+class PageResource(object):
+ def __init__(self, page):
+ self.page = page
+
+ def __acl__(self):
+ return [
+ (Allow, Everyone, 'view'),
+ (Allow, 'role:editor', 'edit'),
+ (Allow, str(self.page.creator_id), 'edit'),
+ ]
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
index 23a5f13f4..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
@@ -2,36 +2,56 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- Page,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import Page, User
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py
index d88c9c71f..25cff7b05 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/security.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py
@@ -1,7 +1,40 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
-GROUPS = {'editor':['group:editors']}
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+)
-def groupfinder(userid, request):
- if userid in USERS:
- return GROUPS.get(userid, [])
+from .models import User
+
+
+class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ user = request.user
+ if user is not None:
+ return user.id
+
+ def effective_principals(self, request):
+ principals = [Everyone]
+ user = request.user
+ if user is not None:
+ principals.append(Authenticated)
+ principals.append(str(user.id))
+ principals.append('role:' + user.role)
+ return principals
+
+def get_user(request):
+ user_id = request.unauthenticated_userid
+ if user_id is not None:
+ user = request.dbsession.query(User).get(user_id)
+ return user
+
+def includeme(config):
+ settings = config.get_settings()
+ authn_policy = MyAuthenticationPolicy(
+ settings['auth.secret'],
+ hashalg='sha512',
+ )
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(ACLAuthorizationPolicy())
+ config.add_request_method(get_user, 'user', reify=True)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt
deleted file mode 100644
index 50e55c850..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt
+++ /dev/null
@@ -1,74 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <p>
- Editing <strong><span tal:replace="page.name">Page Name Goes
- Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
- <p class="pull-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </p>
- <form action="${save_url}" method="post">
- <div class="form-group">
- <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea>
- </div>
- <div class="form-group">
- <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
- </div>
- </form>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
index c0c1b6c20..44d14304e 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
@@ -1,21 +1,20 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -23,31 +22,27 @@
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
+
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
- <p>
- Editing <strong><span tal:replace="page.name">Page Name Goes
- Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
- <form action="${save_url}" method="post">
- <div class="form-group">
- <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea>
- </div>
- <div class="form-group">
- <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
- </div>
- </form>
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2
new file mode 100644
index 000000000..1806de0ff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2
@@ -0,0 +1,26 @@
+{% extends 'layout.jinja2' %}
+
+{% block title %}Login - {% endblock title %}
+
+{% block content %}
+<p>
+<strong>
+ Login
+</strong><br>
+{{ message }}
+</p>
+<form action="{{ url }}" method="post">
+<input type="hidden" name="next" value="{{ next_url }}">
+<div class="form-group">
+ <label for="login">Username</label>
+ <input type="text" name="login" value="{{ login }}">
+</div>
+<div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" name="password">
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt
deleted file mode 100644
index 5f8e9b98c..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
- xmlns:tal="http://xml.zope.org/namespaces/tal">
-<head>
- <title>Login - Pyramid tutorial wiki (based on TurboGears
- 20-Minute Wiki)</title>
- <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
- <meta name="keywords" content="python web application" />
- <meta name="description" content="pyramid web application" />
- <link rel="shortcut icon"
- href="${request.static_url('tutorial:static/favicon.ico')}" />
- <link rel="stylesheet"
- href="${request.static_url('tutorial:static/pylons.css')}"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="${request.static_url('tutorial:static/ie6.css')}"
- type="text/css" media="screen" charset="utf-8" />
- <![endif]-->
-</head>
-<body>
- <div id="wrap">
- <div id="top-small">
- <div class="top-small align-center">
- <div>
- <img width="220" height="50" alt="pyramid"
- src="${request.static_url('tutorial:static/pyramid-small.png')}" />
- </div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-right">
- <div id="left" class="app-welcome align-left">
- <b>Login</b><br/>
- <span tal:replace="message"/>
- </div>
- <div id="right" class="app-welcome align-right"></div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <form action="${url}" method="post">
- <input type="hidden" name="came_from" value="${came_from}"/>
- <input type="text" name="login" value="${login}"/><br/>
- <input type="password" name="password"
- value="${password}"/><br/>
- <input type="submit" name="form.submitted" value="Log In"/>
- </form>
- </div>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py
deleted file mode 100644
index c50e05b6d..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/tests.py
+++ /dev/null
@@ -1,235 +0,0 @@
-import unittest
-import transaction
-
-from pyramid import testing
-
-
-def _initTestingDB():
- from sqlalchemy import create_engine
- from tutorial.models import (
- DBSession,
- Page,
- Base
- )
- engine = create_engine('sqlite://')
- Base.metadata.create_all(engine)
- DBSession.configure(bind=engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
- return DBSession
-
-
-def _registerRoutes(config):
- config.add_route('view_page', '{pagename}')
- config.add_route('edit_page', '{pagename}/edit_page')
- config.add_route('add_page', 'add_page/{pagename}')
-
-
-class ViewWikiTests(unittest.TestCase):
- def setUp(self):
- self.config = testing.setUp()
-
- def tearDown(self):
- testing.tearDown()
-
- def _callFUT(self, request):
- from tutorial.views import view_wiki
- return view_wiki(request)
-
- def test_it(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/FrontPage')
-
-
-class ViewPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
-
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- def _callFUT(self, request):
- from tutorial.views import view_page
- return view_page(request)
-
- def test_it(self):
- from tutorial.models import Page
- request = testing.DummyRequest()
- request.matchdict['pagename'] = 'IDoExist'
- page = Page(name='IDoExist', data='Hello CruelWorld IDoExist')
- self.session.add(page)
- _registerRoutes(self.config)
- info = self._callFUT(request)
- self.assertEqual(info['page'], page)
- self.assertEqual(
- info['content'],
- '<div class="document">\n'
- '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
- 'CruelWorld</a> '
- '<a href="http://example.com/IDoExist">'
- 'IDoExist</a>'
- '</p>\n</div>\n')
- self.assertEqual(info['edit_url'],
- 'http://example.com/IDoExist/edit_page')
-
-
-class AddPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
-
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- def _callFUT(self, request):
- from tutorial.views import add_page
- return add_page(request)
-
- def test_it_notsubmitted(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- request.matchdict = {'pagename':'AnotherPage'}
- info = self._callFUT(request)
- self.assertEqual(info['page'].data,'')
- self.assertEqual(info['save_url'],
- 'http://example.com/add_page/AnotherPage')
-
- def test_it_submitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- request.matchdict = {'pagename':'AnotherPage'}
- self._callFUT(request)
- page = self.session.query(Page).filter_by(name='AnotherPage').one()
- self.assertEqual(page.data, 'Hello yo!')
-
-
-class EditPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
-
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- def _callFUT(self, request):
- from tutorial.views import edit_page
- return edit_page(request)
-
- def test_it_notsubmitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- request.matchdict = {'pagename':'abc'}
- page = Page(name='abc', data='hello')
- self.session.add(page)
- info = self._callFUT(request)
- self.assertEqual(info['page'], page)
- self.assertEqual(info['save_url'],
- 'http://example.com/abc/edit_page')
-
- def test_it_submitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- request.matchdict = {'pagename':'abc'}
- page = Page(name='abc', data='hello')
- self.session.add(page)
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/abc')
- self.assertEqual(page.data, 'Hello yo!')
-
-
-class FunctionalTests(unittest.TestCase):
-
- viewer_login = '/login?login=viewer&password=viewer' \
- '&came_from=FrontPage&form.submitted=Login'
- viewer_wrong_login = '/login?login=viewer&password=incorrect' \
- '&came_from=FrontPage&form.submitted=Login'
- editor_login = '/login?login=editor&password=editor' \
- '&came_from=FrontPage&form.submitted=Login'
-
- def setUp(self):
- from tutorial import main
- settings = { 'sqlalchemy.url': 'sqlite://'}
- app = main({}, **settings)
- from webtest import TestApp
- self.testapp = TestApp(app)
- _initTestingDB()
-
- def tearDown(self):
- del self.testapp
- from tutorial.models import DBSession
- DBSession.remove()
-
- def test_root(self):
- res = self.testapp.get('/', status=302)
- self.assertEqual(res.location, 'http://localhost/FrontPage')
-
- def test_FrontPage(self):
- res = self.testapp.get('/FrontPage', status=200)
- self.assertTrue(b'FrontPage' in res.body)
-
- def test_unexisting_page(self):
- self.testapp.get('/SomePage', status=404)
-
- def test_successful_log_in(self):
- res = self.testapp.get(self.viewer_login, status=302)
- self.assertEqual(res.location, 'http://localhost/FrontPage')
-
- def test_failed_log_in(self):
- res = self.testapp.get(self.viewer_wrong_login, status=200)
- self.assertTrue(b'login' in res.body)
-
- def test_logout_link_present_when_logged_in(self):
- self.testapp.get(self.viewer_login, status=302)
- res = self.testapp.get('/FrontPage', status=200)
- self.assertTrue(b'Logout' in res.body)
-
- def test_logout_link_not_present_after_logged_out(self):
- self.testapp.get(self.viewer_login, status=302)
- self.testapp.get('/FrontPage', status=200)
- res = self.testapp.get('/logout', status=302)
- self.assertTrue(b'Logout' not in res.body)
-
- def test_anonymous_user_cannot_edit(self):
- res = self.testapp.get('/FrontPage/edit_page', status=200)
- self.assertTrue(b'Login' in res.body)
-
- def test_anonymous_user_cannot_add(self):
- res = self.testapp.get('/add_page/NewPage', status=200)
- self.assertTrue(b'Login' in res.body)
-
- def test_viewer_user_cannot_edit(self):
- self.testapp.get(self.viewer_login, status=302)
- res = self.testapp.get('/FrontPage/edit_page', status=200)
- self.assertTrue(b'Login' in res.body)
-
- def test_viewer_user_cannot_add(self):
- self.testapp.get(self.viewer_login, status=302)
- res = self.testapp.get('/add_page/NewPage', status=200)
- self.assertTrue(b'Login' in res.body)
-
- def test_editors_member_user_can_edit(self):
- self.testapp.get(self.editor_login, status=302)
- res = self.testapp.get('/FrontPage/edit_page', status=200)
- self.assertTrue(b'Editing' in res.body)
-
- def test_editors_member_user_can_add(self):
- self.testapp.get(self.editor_login, status=302)
- res = self.testapp.get('/add_page/NewPage', status=200)
- self.assertTrue(b'Editing' in res.body)
-
- def test_editors_member_user_can_view(self):
- self.testapp.get(self.editor_login, status=302)
- res = self.testapp.get('/FrontPage', status=200)
- self.assertTrue(b'FrontPage' in res.body)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
new file mode 100644
index 000000000..715768b2e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
@@ -0,0 +1,122 @@
+import transaction
+import unittest
+import webtest
+
+
+class FunctionalTests(unittest.TestCase):
+
+ basic_login = (
+ '/login?login=basic&password=basic'
+ '&next=FrontPage&form.submitted=Login')
+ basic_wrong_login = (
+ '/login?login=basic&password=incorrect'
+ '&next=FrontPage&form.submitted=Login')
+ editor_login = (
+ '/login?login=editor&password=editor'
+ '&next=FrontPage&form.submitted=Login')
+
+ @classmethod
+ def setUpClass(cls):
+ from tutorial.models.meta import Base
+ from tutorial.models import (
+ User,
+ Page,
+ get_tm_session,
+ )
+ from tutorial import main
+
+ settings = {
+ 'sqlalchemy.url': 'sqlite://',
+ 'auth.secret': 'seekrit',
+ }
+ app = main({}, **settings)
+ cls.testapp = webtest.TestApp(app)
+
+ session_factory = app.registry['dbsession_factory']
+ cls.engine = session_factory.kw['bind']
+ Base.metadata.create_all(bind=cls.engine)
+
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ page1 = Page(name='FrontPage', data='This is the front page')
+ page1.creator = editor
+ page2 = Page(name='BackPage', data='This is the back page')
+ page2.creator = basic
+ dbsession.add_all([basic, editor, page1, page2])
+
+ @classmethod
+ def tearDownClass(cls):
+ from tutorial.models.meta import Base
+ Base.metadata.drop_all(bind=cls.engine)
+
+ def test_root(self):
+ res = self.testapp.get('/', status=302)
+ self.assertEqual(res.location, 'http://localhost/FrontPage')
+
+ def test_FrontPage(self):
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue(b'FrontPage' in res.body)
+
+ def test_unexisting_page(self):
+ self.testapp.get('/SomePage', status=404)
+
+ def test_successful_log_in(self):
+ res = self.testapp.get(self.basic_login, status=302)
+ self.assertEqual(res.location, 'http://localhost/FrontPage')
+
+ def test_failed_log_in(self):
+ res = self.testapp.get(self.basic_wrong_login, status=200)
+ self.assertTrue(b'login' in res.body)
+
+ def test_logout_link_present_when_logged_in(self):
+ self.testapp.get(self.basic_login, status=302)
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue(b'Logout' in res.body)
+
+ def test_logout_link_not_present_after_logged_out(self):
+ self.testapp.get(self.basic_login, status=302)
+ self.testapp.get('/FrontPage', status=200)
+ res = self.testapp.get('/logout', status=302)
+ self.assertTrue(b'Logout' not in res.body)
+
+ def test_anonymous_user_cannot_edit(self):
+ res = self.testapp.get('/FrontPage/edit_page', status=302).follow()
+ self.assertTrue(b'Login' in res.body)
+
+ def test_anonymous_user_cannot_add(self):
+ res = self.testapp.get('/add_page/NewPage', status=302).follow()
+ self.assertTrue(b'Login' in res.body)
+
+ def test_basic_user_cannot_edit_front(self):
+ self.testapp.get(self.basic_login, status=302)
+ res = self.testapp.get('/FrontPage/edit_page', status=302).follow()
+ self.assertTrue(b'Login' in res.body)
+
+ def test_basic_user_can_edit_back(self):
+ self.testapp.get(self.basic_login, status=302)
+ res = self.testapp.get('/BackPage/edit_page', status=200)
+ self.assertTrue(b'Editing' in res.body)
+
+ def test_basic_user_can_add(self):
+ self.testapp.get(self.basic_login, status=302)
+ res = self.testapp.get('/add_page/NewPage', status=200)
+ self.assertTrue(b'Editing' in res.body)
+
+ def test_editors_member_user_can_edit(self):
+ self.testapp.get(self.editor_login, status=302)
+ res = self.testapp.get('/FrontPage/edit_page', status=200)
+ self.assertTrue(b'Editing' in res.body)
+
+ def test_editors_member_user_can_add(self):
+ self.testapp.get(self.editor_login, status=302)
+ res = self.testapp.get('/add_page/NewPage', status=200)
+ self.assertTrue(b'Editing' in res.body)
+
+ def test_editors_member_user_can_view(self):
+ self.testapp.get(self.editor_login, status=302)
+ res = self.testapp.get('/FrontPage', status=200)
+ self.assertTrue(b'FrontPage' in res.body)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py
new file mode 100644
index 000000000..2c945ab33
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py
@@ -0,0 +1,168 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
+ def setUp(self):
+ from ..models import get_tm_session
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('..models')
+ self.config.include('..routes')
+
+ session_factory = self.config.registry['dbsession_factory']
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ self.init_database()
+
+ def init_database(self):
+ from ..models.meta import Base
+ session_factory = self.config.registry['dbsession_factory']
+ engine = session_factory.kw['bind']
+ Base.metadata.create_all(engine)
+
+ def tearDown(self):
+ testing.tearDown()
+ transaction.abort()
+
+ def makeUser(self, name, role, password='dummy'):
+ from ..models import User
+ user = User(name=name, role=role)
+ user.set_password(password)
+ return user
+
+ def makePage(self, name, data, creator):
+ from ..models import Page
+ return Page(name=name, data=data, creator=creator)
+
+
+class ViewWikiTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+ self.config.include('..routes')
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, request):
+ from tutorial.views.default import view_wiki
+ return view_wiki(request)
+
+ def test_it(self):
+ request = testing.DummyRequest()
+ response = self._callFUT(request)
+ self.assertEqual(response.location, 'http://example.com/FrontPage')
+
+
+class ViewPageTests(BaseTest):
+ def _callFUT(self, request):
+ from tutorial.views.default import view_page
+ return view_page(request)
+
+ def test_it(self):
+ from ..routes import PageResource
+
+ # add a page to the db
+ user = self.makeUser('foo', 'editor')
+ page = self.makePage('IDoExist', 'Hello CruelWorld IDoExist', user)
+ self.session.add_all([page, user])
+
+ # create a request asking for the page we've created
+ request = dummy_request(self.session)
+ request.context = PageResource(page)
+
+ # call the view we're testing and check its behavior
+ info = self._callFUT(request)
+ self.assertEqual(info['page'], page)
+ self.assertEqual(
+ info['content'],
+ '<div class="document">\n'
+ '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
+ 'CruelWorld</a> '
+ '<a href="http://example.com/IDoExist">'
+ 'IDoExist</a>'
+ '</p>\n</div>\n')
+ self.assertEqual(info['edit_url'],
+ 'http://example.com/IDoExist/edit_page')
+
+
+class AddPageTests(BaseTest):
+ def _callFUT(self, request):
+ from tutorial.views.default import add_page
+ return add_page(request)
+
+ def test_it_pageexists(self):
+ from ..models import Page
+ from ..routes import NewPage
+ request = testing.DummyRequest({'form.submitted': True,
+ 'body': 'Hello yo!'},
+ dbsession=self.session)
+ request.user = self.makeUser('foo', 'editor')
+ request.context = NewPage('AnotherPage')
+ self._callFUT(request)
+ pagecount = self.session.query(Page).filter_by(name='AnotherPage').count()
+ self.assertGreater(pagecount, 0)
+
+ def test_it_notsubmitted(self):
+ from ..routes import NewPage
+ request = dummy_request(self.session)
+ request.user = self.makeUser('foo', 'editor')
+ request.context = NewPage('AnotherPage')
+ info = self._callFUT(request)
+ self.assertEqual(info['pagedata'], '')
+ self.assertEqual(info['save_url'],
+ 'http://example.com/add_page/AnotherPage')
+
+ def test_it_submitted(self):
+ from ..models import Page
+ from ..routes import NewPage
+ request = testing.DummyRequest({'form.submitted': True,
+ 'body': 'Hello yo!'},
+ dbsession=self.session)
+ request.user = self.makeUser('foo', 'editor')
+ request.context = NewPage('AnotherPage')
+ self._callFUT(request)
+ page = self.session.query(Page).filter_by(name='AnotherPage').one()
+ self.assertEqual(page.data, 'Hello yo!')
+
+
+class EditPageTests(BaseTest):
+ def _callFUT(self, request):
+ from tutorial.views.default import edit_page
+ return edit_page(request)
+
+ def makeContext(self, page):
+ from ..routes import PageResource
+ return PageResource(page)
+
+ def test_it_notsubmitted(self):
+ user = self.makeUser('foo', 'editor')
+ page = self.makePage('abc', 'hello', user)
+ self.session.add_all([page, user])
+
+ request = dummy_request(self.session)
+ request.context = self.makeContext(page)
+ info = self._callFUT(request)
+ self.assertEqual(info['pagename'], 'abc')
+ self.assertEqual(info['save_url'],
+ 'http://example.com/abc/edit_page')
+
+ def test_it_submitted(self):
+ user = self.makeUser('foo', 'editor')
+ page = self.makePage('abc', 'hello', user)
+ self.session.add_all([page, user])
+
+ request = testing.DummyRequest({'form.submitted': True,
+ 'body': 'Hello yo!'},
+ dbsession=self.session)
+ request.context = self.makeContext(page)
+ response = self._callFUT(request)
+ self.assertEqual(response.location, 'http://example.com/abc')
+ self.assertEqual(page.data, 'Hello yo!')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py
deleted file mode 100644
index 41bea4785..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/views.py
+++ /dev/null
@@ -1,123 +0,0 @@
-import re
-from docutils.core import publish_parts
-
-from pyramid.httpexceptions import (
- HTTPFound,
- HTTPNotFound,
- )
-
-from pyramid.view import (
- view_config,
- forbidden_view_config,
- )
-
-from pyramid.security import (
- remember,
- forget,
- )
-
-from .security import USERS
-
-from .models import (
- DBSession,
- Page,
- )
-
-
-# regular expression used to find WikiWords
-wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-
-@view_config(route_name='view_wiki',
- permission='view')
-def view_wiki(request):
- return HTTPFound(location = request.route_url('view_page',
- pagename='FrontPage'))
-
-@view_config(route_name='view_page', renderer='templates/view.pt',
- permission='view')
-def view_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).first()
- if page is None:
- return HTTPNotFound('No such page')
-
- def check(match):
- word = match.group(1)
- exists = DBSession.query(Page).filter_by(name=word).all()
- if exists:
- view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, word)
- else:
- add_url = request.route_url('add_page', pagename=word)
- return '<a href="%s">%s</a>' % (add_url, word)
-
- content = publish_parts(page.data, writer_name='html')['html_body']
- content = wikiwords.sub(check, content)
- edit_url = request.route_url('edit_page', pagename=pagename)
- return dict(page=page, content=content, edit_url=edit_url,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='add_page', renderer='templates/edit.pt',
- permission='edit')
-def add_page(request):
- pagename = request.matchdict['pagename']
- if 'form.submitted' in request.params:
- body = request.params['body']
- page = Page(name=pagename, data=body)
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- save_url = request.route_url('add_page', pagename=pagename)
- page = Page(name='', data='')
- return dict(page=page, save_url=save_url,
- logged_in=request.authenticated_userid)
-
-@view_config(route_name='edit_page', renderer='templates/edit.pt',
- permission='edit')
-def edit_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).one()
- if 'form.submitted' in request.params:
- page.data = request.params['body']
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- return dict(
- page=page,
- save_url=request.route_url('edit_page', pagename=pagename),
- logged_in=request.authenticated_userid
- )
-
-@view_config(route_name='login', renderer='templates/login.pt')
-@forbidden_view_config(renderer='templates/login.pt')
-def login(request):
- login_url = request.route_url('login')
- referrer = request.url
- if referrer == login_url:
- referrer = '/' # never use the login form itself as came_from
- came_from = request.params.get('came_from', referrer)
- message = ''
- login = ''
- password = ''
- if 'form.submitted' in request.params:
- login = request.params['login']
- password = request.params['password']
- if USERS.get(login) == password:
- headers = remember(request, login)
- return HTTPFound(location = came_from,
- headers = headers)
- message = 'Failed login'
-
- return dict(
- message = message,
- url = request.application_url + '/login',
- came_from = came_from,
- login = login,
- password = password,
- )
-
-@view_config(route_name='logout')
-def logout(request):
- headers = forget(request)
- return HTTPFound(location = request.route_url('view_wiki'),
- headers = headers)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py
new file mode 100644
index 000000000..2b993b430
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py
@@ -0,0 +1,46 @@
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import (
+ remember,
+ forget,
+ )
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..models import User
+
+
+@view_config(route_name='login', renderer='../templates/login.jinja2')
+def login(request):
+ next_url = request.params.get('next', request.referrer)
+ if not next_url:
+ next_url = request.route_url('view_wiki')
+ message = ''
+ login = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ user = request.dbsession.query(User).filter_by(name=login).first()
+ if user is not None and user.check_password(password):
+ headers = remember(request, user.id)
+ return HTTPFound(location=next_url, headers=headers)
+ message = 'Failed login'
+
+ return dict(
+ message=message,
+ url=request.route_url('login'),
+ next_url=next_url,
+ login=login,
+ )
+
+@view_config(route_name='logout')
+def logout(request):
+ headers = forget(request)
+ next_url = request.route_url('view_wiki')
+ return HTTPFound(location=next_url, headers=headers)
+
+@forbidden_view_config()
+def forbidden_view(request):
+ next_url = request.route_url('login', _query={'next': request.url})
+ return HTTPFound(location=next_url)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
new file mode 100644
index 000000000..9358993ea
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
@@ -0,0 +1,64 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+from ..models import Page
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2',
+ permission='view')
+def view_page(request):
+ page = request.context.page
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=page.name)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2',
+ permission='edit')
+def edit_page(request):
+ page = request.context.page
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2',
+ permission='create')
+def add_page(request):
+ pagename = request.context.pagename
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = request.user
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/src/views/MANIFEST.in b/docs/tutorials/wiki2/src/views/MANIFEST.in
index 81beba1b1..42cd299b5 100644
--- a/docs/tutorials/wiki2/src/views/MANIFEST.in
+++ b/docs/tutorials/wiki2/src/views/MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt
index 68f430110..5b0101e5f 100644
--- a/docs/tutorials/wiki2/src/views/README.txt
+++ b/docs/tutorials/wiki2/src/views/README.txt
@@ -6,7 +6,7 @@ Getting Started
- cd <directory containing this file>
-- $VENV/bin/python setup.py develop
+- $VENV/bin/pip install -e .
- $VENV/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini
index a9d53b296..22b733e10 100644
--- a/docs/tutorials/wiki2/src/views/development.ini
+++ b/docs/tutorials/wiki2/src/views/development.ini
@@ -1,6 +1,6 @@
###
# app configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
###
[app:main]
@@ -27,12 +27,12 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
+host = 127.0.0.1
port = 6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
###
[loggers]
@@ -68,4 +68,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini
index 4684d2f7a..d2ecfe22a 100644
--- a/docs/tutorials/wiki2/src/views/production.ini
+++ b/docs/tutorials/wiki2/src/views/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,8 +11,6 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
@@ -16,7 +19,10 @@ use = egg:waitress#main
host = 0.0.0.0
port = 6543
-# Begin logging configuration
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +57,4 @@ level = NOTSET
formatter = generic
[formatter_generic]
-format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
-
-# End logging configuration
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py
index 09bd63d33..def3ce1f6 100644
--- a/docs/tutorials/wiki2/src/views/setup.py
+++ b/docs/tutorials/wiki2/src/views/setup.py
@@ -9,15 +9,22 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
]
setup(name='tutorial',
@@ -25,11 +32,11 @@ setup(name='tutorial',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -37,7 +44,9 @@ setup(name='tutorial',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- test_suite='tutorial',
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
index 37cae1997..4dab44823 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
@@ -1,24 +1,12 @@
from pyramid.config import Configurator
-from sqlalchemy import engine_from_config
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
config = Configurator(settings=settings)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('view_wiki', '/')
- config.add_route('view_page', '/{pagename}')
- config.add_route('add_page', '/add_page/{pagename}')
- config.add_route('edit_page', '/{pagename}/edit_page')
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models.py b/docs/tutorials/wiki2/src/views/tutorial/models.py
deleted file mode 100644
index f028c917a..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/models.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from sqlalchemy import (
- Column,
- Integer,
- Text,
- )
-
-from sqlalchemy.ext.declarative import declarative_base
-
-from sqlalchemy.orm import (
- scoped_session,
- sessionmaker,
- )
-
-from zope.sqlalchemy import ZopeTransactionExtension
-
-DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
-Base = declarative_base()
-
-
-class Page(Base):
- """ The SQLAlchemy declarative model class for a Page object. """
- __tablename__ = 'pages'
- id = Column(Integer, primary_key=True)
- name = Column(Text, unique=True)
- data = Column(Text)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
new file mode 100644
index 000000000..a8871f6f5
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('tutorial.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/page.py b/docs/tutorials/wiki2/src/views/tutorial/models/page.py
new file mode 100644
index 000000000..4dd5b5721
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/page.py
@@ -0,0 +1,20 @@
+from sqlalchemy import (
+ Column,
+ ForeignKey,
+ Integer,
+ Text,
+)
+from sqlalchemy.orm import relationship
+
+from .meta import Base
+
+
+class Page(Base):
+ """ The SQLAlchemy declarative model class for a Page object. """
+ __tablename__ = 'pages'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ data = Column(Integer, nullable=False)
+
+ creator_id = Column(ForeignKey('users.id'), nullable=False)
+ creator = relationship('User', backref='created_pages')
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py
new file mode 100644
index 000000000..6bd3315d6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py
@@ -0,0 +1,29 @@
+import bcrypt
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class User(Base):
+ """ The SQLAlchemy declarative model class for a User object. """
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text, nullable=False, unique=True)
+ role = Column(Text, nullable=False)
+
+ password_hash = Column(Text)
+
+ def set_password(self, pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ self.password_hash = pwhash
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash
+ actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
+ return expected_hash == actual_hash
+ return False
diff --git a/docs/tutorials/wiki2/src/views/tutorial/routes.py b/docs/tutorials/wiki2/src/views/tutorial/routes.py
new file mode 100644
index 000000000..72df58efe
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/routes.py
@@ -0,0 +1,6 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('view_wiki', '/')
+ config.add_route('view_page', '/{pagename}')
+ config.add_route('add_page', '/add_page/{pagename}')
+ config.add_route('edit_page', '/{pagename}/edit_page')
diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
index 23a5f13f4..f3c0a6fef 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
@@ -2,36 +2,56 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
)
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
from ..models import (
- DBSession,
- Page,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import Page, User
def usage(argv):
cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
+ print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
- if len(argv) != 2:
+ if len(argv) < 2:
usage(argv)
config_uri = argv[1]
+ options = parse_vars(argv[2:])
setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ editor = User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/views/tutorial/static/theme.min.css
deleted file mode 100644
index 0d25de5b6..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2
new file mode 100644
index 000000000..37b0a16b6
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2
new file mode 100644
index 000000000..7db25c674
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2
@@ -0,0 +1,20 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
+
+{% block content %}
+<p>
+Editing <strong>{{pagename}}</strong>
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+<form action="{{ save_url }}" method="post">
+<div class="form-group">
+ <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
index 4e5772de0..71785157f 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
@@ -1,21 +1,20 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
- <title>${page.name} - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -23,36 +22,18 @@
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
+
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- <p>
- Viewing <strong><span tal:replace="page.name">
- Page Name Goes Here</span></strong>
- </p>
- <p>You can return to the
- <a href="${request.application_url}">FrontPage</a>.
- </p>
- <p class="pull-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </p>
+ {% block content %}{% endblock %}
</div>
</div>
</div>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt
deleted file mode 100644
index c9b0cec21..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,66 +0,0 @@
-<!DOCTYPE html>
-<html lang="${request.locale_name}">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="pyramid web application">
- <meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
-
- <title>Alchemy Scaffold for The Pyramid Web Framework</title>
-
- <!-- Bootstrap core CSS -->
- <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Custom styles for this scaffold -->
- <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
- <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
- <![endif]-->
- </head>
-
- <body>
-
- <div class="starter-template">
- <div class="container">
- <div class="row">
- <div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
- </div>
- <div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework</span>.</p>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="links">
- <ul>
- <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/latest/">Docs</a></li>
- <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
- <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li>
- <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
- </ul>
- </div>
- </div>
- <div class="row">
- <div class="copyright">
- Copyright &copy; Pylons Project
- </div>
- </div>
- </div>
- </div>
-
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
- <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py
index 9f01d2da5..99e95efd3 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py
@@ -3,144 +3,63 @@ import transaction
from pyramid import testing
-def _initTestingDB():
- from sqlalchemy import create_engine
- from tutorial.models import (
- DBSession,
- Page,
- Base
- )
- engine = create_engine('sqlite://')
- Base.metadata.create_all(engine)
- DBSession.configure(bind=engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
- return DBSession
-
-def _registerRoutes(config):
- config.add_route('view_page', '{pagename}')
- config.add_route('edit_page', '{pagename}/edit_page')
- config.add_route('add_page', 'add_page/{pagename}')
-
-class ViewWikiTests(unittest.TestCase):
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- self.session = _initTestingDB()
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
- def _callFUT(self, request):
- from tutorial.views import view_wiki
- return view_wiki(request)
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
- def test_it(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/FrontPage')
+ self.session = get_tm_session(session_factory, transaction.manager)
-class ViewPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- self.session.remove()
- testing.tearDown()
-
- def _callFUT(self, request):
- from tutorial.views import view_page
- return view_page(request)
-
- def test_it(self):
- from tutorial.models import Page
- request = testing.DummyRequest()
- request.matchdict['pagename'] = 'IDoExist'
- page = Page(name='IDoExist', data='Hello CruelWorld IDoExist')
- self.session.add(page)
- _registerRoutes(self.config)
- info = self._callFUT(request)
- self.assertEqual(info['page'], page)
- self.assertEqual(
- info['content'],
- '<div class="document">\n'
- '<p>Hello <a href="http://example.com/add_page/CruelWorld">'
- 'CruelWorld</a> '
- '<a href="http://example.com/IDoExist">'
- 'IDoExist</a>'
- '</p>\n</div>\n')
- self.assertEqual(info['edit_url'],
- 'http://example.com/IDoExist/edit_page')
-
-
-class AddPageTests(unittest.TestCase):
- def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
+ from .models.meta import Base
- def tearDown(self):
- self.session.remove()
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
- def _callFUT(self, request):
- from tutorial.views import add_page
- return add_page(request)
-
- def test_it_notsubmitted(self):
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- request.matchdict = {'pagename':'AnotherPage'}
- info = self._callFUT(request)
- self.assertEqual(info['page'].data,'')
- self.assertEqual(info['save_url'],
- 'http://example.com/add_page/AnotherPage')
-
- def test_it_submitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- request.matchdict = {'pagename':'AnotherPage'}
- self._callFUT(request)
- page = self.session.query(Page).filter_by(name='AnotherPage').one()
- self.assertEqual(page.data, 'Hello yo!')
-
-class EditPageTests(unittest.TestCase):
def setUp(self):
- self.session = _initTestingDB()
- self.config = testing.setUp()
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
- def tearDown(self):
- self.session.remove()
- testing.tearDown()
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], 'tutorial')
+
+
+class TestMyViewFailureCondition(BaseTest):
- def _callFUT(self, request):
- from tutorial.views import edit_page
- return edit_page(request)
-
- def test_it_notsubmitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest()
- request.matchdict = {'pagename':'abc'}
- page = Page(name='abc', data='hello')
- self.session.add(page)
- info = self._callFUT(request)
- self.assertEqual(info['page'], page)
- self.assertEqual(info['save_url'],
- 'http://example.com/abc/edit_page')
-
- def test_it_submitted(self):
- from tutorial.models import Page
- _registerRoutes(self.config)
- request = testing.DummyRequest({'form.submitted':True,
- 'body':'Hello yo!'})
- request.matchdict = {'pagename':'abc'}
- page = Page(name='abc', data='hello')
- self.session.add(page)
- response = self._callFUT(request)
- self.assertEqual(response.location, 'http://example.com/abc')
- self.assertEqual(page.data, 'Hello yo!')
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py
deleted file mode 100644
index a3707dab5..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/views.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import cgi
-import re
-from docutils.core import publish_parts
-
-from pyramid.httpexceptions import (
- HTTPFound,
- HTTPNotFound,
- )
-
-from pyramid.view import view_config
-
-from .models import (
- DBSession,
- Page,
- )
-
-# regular expression used to find WikiWords
-wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-
-@view_config(route_name='view_wiki')
-def view_wiki(request):
- return HTTPFound(location = request.route_url('view_page',
- pagename='FrontPage'))
-
-@view_config(route_name='view_page', renderer='templates/view.pt')
-def view_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).first()
- if page is None:
- return HTTPNotFound('No such page')
-
- def check(match):
- word = match.group(1)
- exists = DBSession.query(Page).filter_by(name=word).all()
- if exists:
- view_url = request.route_url('view_page', pagename=word)
- return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
- else:
- add_url = request.route_url('add_page', pagename=word)
- return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
-
- content = publish_parts(page.data, writer_name='html')['html_body']
- content = wikiwords.sub(check, content)
- edit_url = request.route_url('edit_page', pagename=pagename)
- return dict(page=page, content=content, edit_url=edit_url)
-
-@view_config(route_name='add_page', renderer='templates/edit.pt')
-def add_page(request):
- pagename = request.matchdict['pagename']
- if 'form.submitted' in request.params:
- body = request.params['body']
- page = Page(name=pagename, data=body)
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- save_url = request.route_url('add_page', pagename=pagename)
- page = Page(name='', data='')
- return dict(page=page, save_url=save_url)
-
-@view_config(route_name='edit_page', renderer='templates/edit.pt')
-def edit_page(request):
- pagename = request.matchdict['pagename']
- page = DBSession.query(Page).filter_by(name=pagename).one()
- if 'form.submitted' in request.params:
- page.data = request.params['body']
- DBSession.add(page)
- return HTTPFound(location = request.route_url('view_page',
- pagename=pagename))
- return dict(
- page=page,
- save_url = request.route_url('edit_page', pagename=pagename),
- )
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
new file mode 100644
index 000000000..bb6300b75
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
@@ -0,0 +1,73 @@
+import cgi
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import (
+ HTTPFound,
+ HTTPNotFound,
+ )
+
+from pyramid.view import view_config
+
+from ..models import Page, User
+
+# regular expression used to find WikiWords
+wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
+
+@view_config(route_name='view_wiki')
+def view_wiki(request):
+ next_url = request.route_url('view_page', pagename='FrontPage')
+ return HTTPFound(location=next_url)
+
+@view_config(route_name='view_page', renderer='../templates/view.jinja2')
+def view_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ if page is None:
+ raise HTTPNotFound('No such page')
+
+ def add_link(match):
+ word = match.group(1)
+ exists = request.dbsession.query(Page).filter_by(name=word).all()
+ if exists:
+ view_url = request.route_url('view_page', pagename=word)
+ return '<a href="%s">%s</a>' % (view_url, cgi.escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, cgi.escape(word))
+
+ content = publish_parts(page.data, writer_name='html')['html_body']
+ content = wikiwords.sub(add_link, content)
+ edit_url = request.route_url('edit_page', pagename=page.name)
+ return dict(page=page, content=content, edit_url=edit_url)
+
+@view_config(route_name='edit_page', renderer='../templates/edit.jinja2')
+def edit_page(request):
+ pagename = request.matchdict['pagename']
+ page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ if 'form.submitted' in request.params:
+ page.data = request.params['body']
+ next_url = request.route_url('view_page', pagename=page.name)
+ return HTTPFound(location=next_url)
+ return dict(
+ pagename=page.name,
+ pagedata=page.data,
+ save_url=request.route_url('edit_page', pagename=page.name),
+ )
+
+@view_config(route_name='add_page', renderer='../templates/edit.jinja2')
+def add_page(request):
+ pagename = request.matchdict['pagename']
+ if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ next_url = request.route_url('edit_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ if 'form.submitted' in request.params:
+ body = request.params['body']
+ page = Page(name=pagename, data=body)
+ page.creator = (
+ request.dbsession.query(User).filter_by(name='editor').one())
+ request.dbsession.add(page)
+ next_url = request.route_url('view_page', pagename=pagename)
+ return HTTPFound(location=next_url)
+ save_url = request.route_url('add_page', pagename=pagename)
+ return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst
index 9db95334a..e923ff9cb 100644
--- a/docs/tutorials/wiki2/tests.rst
+++ b/docs/tutorials/wiki2/tests.rst
@@ -1,97 +1,108 @@
+.. _wiki2_adding_tests:
+
============
Adding Tests
============
-We will now add tests for the models and the views and a few functional tests
-in ``tests.py``. Tests ensure that an application works, and that it
-continues to work when changes are made in the future.
+We will now add tests for the models and views as well as a few functional
+tests in a new ``tests`` subpackage. Tests ensure that an application works,
+and that it continues to work when changes are made in the future.
+
+The file ``tests.py`` was generated as part of the ``alchemy`` scaffold, but it
+is a common practice to put tests into a ``tests`` subpackage, especially as
+projects grow in size and complexity. Each module in the test subpackage
+should contain tests for its corresponding module in our application. Each
+corresponding pair of modules should have the same names, except the test
+module should have the prefix ``test_``.
+
+Start by deleting ``tests.py``, then create a new directory to contain our new
+tests as well as a new empty file ``tests/__init__.py``.
-Test the models
-===============
+.. warning::
+
+ It is very important when refactoring a Python module into a package to be
+ sure to delete the cache files (``.pyc`` files or ``__pycache__`` folders)
+ sitting around! Python will prioritize the cache files before traversing
+ into folders, using the old code, and you will wonder why none of your
+ changes are working!
-To test the model class ``Page`` we'll add a new ``PageModelTests`` class to
-our ``tests.py`` file that was generated as part of the ``alchemy`` scaffold.
Test the views
==============
-We'll modify our ``tests.py`` file, adding tests for each view function we
-added previously. As a result, we'll *delete* the ``ViewTests`` class that
-the ``alchemy`` scaffold provided, and add four other test classes:
+We'll create a new ``tests/test_views.py`` file, adding a ``BaseTest`` class
+used as the base for other test classes. Next we'll add tests for each view
+function we previously added to our application. We'll add four test classes:
``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``.
These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page``
views.
+
Functional tests
================
-We'll test the whole application, covering security aspects that are not
-tested in the unit tests, like logging in, logging out, checking that
-the ``viewer`` user cannot add or edit pages, but the ``editor`` user
-can, and so on.
+We'll test the whole application, covering security aspects that are not tested
+in the unit tests, like logging in, logging out, checking that the ``basic``
+user cannot edit pages that it didn't create but the ``editor`` user can, and
+so on.
+
-View the results of all our edits to ``tests.py``
-=================================================
+View the results of all our edits to ``tests`` subpackage
+=========================================================
-Open the ``tutorial/tests.py`` module, and edit it such that it appears as
+Open ``tutorial/tests/test_views.py``, and edit it such that it appears as
follows:
-.. literalinclude:: src/tests/tutorial/tests.py
+.. literalinclude:: src/tests/tutorial/tests/test_views.py
:linenos:
:language: python
-Running the tests
-=================
-
-We can run these tests by using ``setup.py test`` in the same way we did in
-:ref:`running_tests`. However, first we must edit our ``setup.py`` to
-include a dependency on WebTest, which we've used in our ``tests.py``.
-Change the ``requires`` list in ``setup.py`` to include ``WebTest``.
+Open ``tutorial/tests/test_functional.py``, and edit it such that it appears as
+follows:
-.. literalinclude:: src/tests/setup.py
+.. literalinclude:: src/tests/tutorial/tests/test_functional.py
:linenos:
:language: python
- :lines: 11-22
- :emphasize-lines: 11
-After we've added a dependency on WebTest in ``setup.py``, we need to run
-``setup.py develop`` to get WebTest installed into our virtualenv. Assuming
-our shell's current working directory is the "tutorial" distribution
-directory:
-On UNIX:
+.. note::
-.. code-block:: text
-
- $ $VENV/bin/python setup.py develop
+ We're utilizing the excellent WebTest_ package to do functional testing of
+ the application. This is defined in the ``tests_require`` section of our
+ ``setup.py``. Any other dependencies needed only for testing purposes can be
+ added there and will be installed automatically when running
+ ``setup.py test``.
-On Windows:
-
-.. code-block:: text
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+Running the tests
+=================
-Once that command has completed successfully, we can run the tests
-themselves:
+We can run these tests similarly to how we did in :ref:`running_tests`:
On UNIX:
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/python setup.py test -q
+ $ $VENV/bin/py.test -q
On Windows:
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
+ c:\pyramidtut\tutorial> %VENV%\Scripts\py.test -q
The expected result should look like the following:
.. code-block:: text
......................
- ----------------------------------------------------------------------
- Ran 21 tests in 2.700s
+ 22 passed, 1 pytest-warnings in 5.81 seconds
+
+.. note:: If you use Python 3 during this tutorial, you will see deprecation
+ warnings in the output, which we will choose to ignore. In making this
+ tutorial run on both Python 2 and 3, the authors prioritized simplicity and
+ focus for the learner over accommodating warnings. In your own app or as
+ extra credit, you may choose to either drop Python 2 support or hack your
+ code to work without warnings on both Python 2 and 3.
- OK
+.. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/
diff --git a/docs/whatsnew-1.6.rst b/docs/whatsnew-1.6.rst
index f5c307b5d..77d89b017 100644
--- a/docs/whatsnew-1.6.rst
+++ b/docs/whatsnew-1.6.rst
@@ -98,7 +98,7 @@ Feature Additions
relative to the top-level package. See
https://github.com/Pylons/pyramid/pull/1337
-- Overall improvments for the ``proutes`` command. Added ``--format`` and
+- Overall improvements for the ``proutes`` command. Added ``--format`` and
``--glob`` arguments to the command, introduced the ``method``
column for displaying available request methods, and improved the ``view``
output by showing the module instead of just ``__repr__``. See
diff --git a/docs/whatsnew-1.7.rst b/docs/whatsnew-1.7.rst
new file mode 100644
index 000000000..fd144a24a
--- /dev/null
+++ b/docs/whatsnew-1.7.rst
@@ -0,0 +1,172 @@
+What's New in Pyramid 1.7
+=========================
+
+This article explains the new features in :app:`Pyramid` version 1.7 as
+compared to its predecessor, :app:`Pyramid` 1.6. It also documents backwards
+incompatibilities between the two versions and deprecations added to
+:app:`Pyramid` 1.7, as well as software dependency changes and notable
+documentation additions.
+
+Backwards Incompatibilities
+---------------------------
+
+- The default hash algorithm for
+ :class:`pyramid.authentication.AuthTktAuthenticationPolicy` has changed from
+ ``md5`` to ``sha512``. If you are using the authentication policy and need to
+ continue using ``md5``, please explicitly set ``hashalg='md5'``.
+
+ If you are not currently specifying the ``hashalg`` option in your apps, then
+ this change means any existing auth tickets (and associated cookies) will no
+ longer be valid, users will be logged out, and have to login to their
+ accounts again.
+
+ This change has been issuing a DeprecationWarning since :app:`Pyramid` 1.4.
+
+ See https://github.com/Pylons/pyramid/pull/2496
+
+- Python 2.6 and 3.2 are no longer supported by Pyramid. See
+ https://github.com/Pylons/pyramid/issues/2368 and
+ https://github.com/Pylons/pyramid/pull/2256
+
+- The :func:`pyramid.session.check_csrf_token` function no longer validates a
+ csrf token in the query string of a request. Only headers and request bodies
+ are supported. See https://github.com/Pylons/pyramid/pull/2500
+
+Feature Additions
+-----------------
+
+- A new :ref:`view_derivers` concept has been added to Pyramid to allow
+ framework authors to inject elements into the standard Pyramid view pipeline
+ and affect all views in an application. This is similar to a decorator except
+ that it has access to options passed to ``config.add_view`` and can affect
+ other stages of the pipeline such as the raw response from a view or prior
+ to security checks. See https://github.com/Pylons/pyramid/pull/2021
+
+- Added a new setting, ``pyramid.require_default_csrf`` which may be used
+ to turn on CSRF checks globally for every request in the application.
+ This should be considered a good default for websites built on Pyramid.
+ It is possible to opt-out of CSRF checks on a per-view basis by setting
+ ``require_csrf=False`` on those views.
+ See :ref:`auto_csrf_checking` and
+ https://github.com/Pylons/pyramid/pull/2413
+
+- Added a ``require_csrf`` view option which will enforce CSRF checks on
+ requests with an unsafe method as defined by RFC2616. If the CSRF check fails
+ a ``BadCSRFToken`` exception will be raised and may be caught by exception
+ views (the default response is a ``400 Bad Request``). This option should be
+ used in place of the deprecated ``check_csrf`` view predicate which would
+ normally result in unexpected ``404 Not Found`` response to the client
+ instead of a catchable exception. See :ref:`auto_csrf_checking`,
+ https://github.com/Pylons/pyramid/pull/2413 and
+ https://github.com/Pylons/pyramid/pull/2500
+
+- Added an additional CSRF validation that checks the origin/referrer of a
+ request and makes sure it matches the current ``request.domain``. This
+ particular check is only active when accessing a site over HTTPS as otherwise
+ browsers don't always send the required information. If this additional CSRF
+ validation fails a ``BadCSRFOrigin`` exception will be raised and may be
+ caught by exception views (the default response is ``400 Bad Request``).
+ Additional allowed origins may be configured by setting
+ ``pyramid.csrf_trusted_origins`` to a list of domain names (with ports if on
+ a non standard port) to allow. Subdomains are not allowed unless the domain
+ name has been prefixed with a ``.``. See
+ https://github.com/Pylons/pyramid/pull/2501
+
+- Added a new :func:`pyramid.session.check_csrf_origin` API for validating the
+ origin or referrer headers against the request's domain.
+ See https://github.com/Pylons/pyramid/pull/2501
+
+- Subclasses of :class:`pyramid.httpexceptions.HTTPException` will now take
+ into account the best match for the clients ``Accept`` header, and depending
+ on what is requested will return ``text/html``, ``application/json`` or
+ ``text/plain``. The default for ``*/*`` is still ``text/html``, but if
+ ``application/json`` is explicitly mentioned it will now receive a valid
+ JSON response. See https://github.com/Pylons/pyramid/pull/2489
+
+- A new event, :class:`pyramid.events.BeforeTraversal`, and interface
+ :class:`pyramid.interfaces.IBeforeTraversal` have been introduced that will
+ notify listeners before traversal starts in the router.
+ See :ref:`router_chapter` as well as
+ https://github.com/Pylons/pyramid/pull/2469 and
+ https://github.com/Pylons/pyramid/pull/1876
+
+- A new method, :meth:`pyramid.request.Request.invoke_exception_view`, which
+ can be used to invoke an exception view and get back a response. This is
+ useful for rendering an exception view outside of the context of the
+ ``EXCVIEW`` tween where you may need more control over the request.
+ See https://github.com/Pylons/pyramid/pull/2393
+
+- Allow a leading ``=`` on the key of the request param predicate.
+ For example, ``'=abc=1'`` is equivalent down to
+ ``request.params['=abc'] == '1'``.
+ See https://github.com/Pylons/pyramid/pull/1370
+
+- Allow using variable substitutions like ``%(LOGGING_LOGGER_ROOT_LEVEL)s``
+ for logging sections of the .ini file and populate these variables from
+ the ``pserve`` command line -- e.g.:
+
+ ``pserve development.ini LOGGING_LOGGER_ROOT_LEVEL=DEBUG``
+
+ This support is thanks to the new ``global_conf`` option on
+ :func:`pyramid.paster.setup_logging`.
+ See https://github.com/Pylons/pyramid/pull/2399
+
+Deprecations
+------------
+
+- The ``check_csrf`` view predicate has been deprecated. Use the
+ new ``require_csrf`` option or the ``pyramid.require_default_csrf`` setting
+ to ensure that the :class:`pyramid.exceptions.BadCSRFToken` exception is
+ raised. See https://github.com/Pylons/pyramid/pull/2413
+
+- Support for Python 3.3 will be removed in Pyramid 1.8.
+ https://github.com/Pylons/pyramid/issues/2477
+
+Scaffolding Enhancements
+------------------------
+
+- A complete overhaul of the ``alchemy`` scaffold to show more modern best
+ practices with regards to SQLAlchemy session management, as well as a more
+ modular approach to configuration, separating routes into a separate module
+ to illustrate uses of :meth:`pyramid.config.Configurator.include`.
+ See https://github.com/Pylons/pyramid/pull/2024
+
+Documentation Enhancements
+--------------------------
+
+A massive overhaul of the packaging and tools used in the documentation
+was completed in https://github.com/Pylons/pyramid/pull/2468. A summary
+follows:
+
+- All docs now recommend using ``pip`` instead of ``easy_install``.
+
+- The installation docs now expect the user to be using Python 3.4 or
+ greater with access to the ``python3 -m venv`` tool to create virtual
+ environments.
+
+- Tutorials now use ``py.test`` and ``pytest-cov`` instead of ``nose`` and
+ ``coverage``.
+
+- Further updates to the scaffolds as well as tutorials and their src files.
+
+Along with the overhaul of the ``alchemy`` scaffold came a total overhaul
+of the :ref:`bfg_sql_wiki_tutorial` tutorial to introduce more modern
+features into the usage of SQLAlchemy with Pyramid and provide a better
+starting point for new projects. See
+https://github.com/Pylons/pyramid/pull/2024 for more. Highlights were:
+
+- New SQLAlchemy session management without any global ``DBSession``. Replaced
+ by a per-request ``request.dbsession`` property.
+
+- A new authentication chapter demonstrating how to get simple authentication
+ bootstrapped quickly in an application.
+
+- Authorization was overhauled to show the use of per-route context factories
+ which demonstrate object-level authorization on top of simple group-level
+ authorization. Did you want to restrict page edits to only the owner but
+ couldn't figure it out before? Here you go!
+
+- The users and groups are stored in the database now instead of within
+ tutorial-specific global variables.
+
+- User passwords are stored using ``bcrypt``.
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index 9bf1de62e..e6b888db2 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -5,7 +5,6 @@ import hashlib
import base64
import re
import time as time_mod
-import warnings
from zope.interface import implementer
@@ -417,20 +416,11 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy):
be done somewhere else or in a subclass."""
return []
-_marker = object()
-
@implementer(IAuthenticationPolicy)
class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
"""A :app:`Pyramid` :term:`authentication policy` which
obtains data from a Pyramid "auth ticket" cookie.
- .. warning::
-
- The default hash algorithm used in this policy is MD5 and has known
- hash collision vulnerabilities. The risk of an exploit is low.
- However, for improved authentication security, use
- ``hashalg='sha512'``.
-
Constructor Arguments
``secret``
@@ -552,7 +542,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
``hashalg``
- Default: ``md5`` (the literal string).
+ Default: ``sha512`` (the literal string).
Any hash algorithm supported by Python's ``hashlib.new()`` function
can be used as the ``hashalg``.
@@ -562,21 +552,10 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
``hashalg`` will imply that all existing users with a valid cookie will
be required to re-login.
- A warning is emitted at startup if an explicit ``hashalg`` is not
- passed. This is for backwards compatibility reasons.
-
This option is available as of :app:`Pyramid` 1.4.
Optional.
- .. note::
-
- ``md5`` is the default for backwards compatibility reasons. However,
- if you don't specify ``md5`` as the hashalg explicitly, a warning is
- issued at application startup time. An explicit value of ``sha512``
- is recommended for improved security, and ``sha512`` will become the
- default in a future Pyramid version.
-
``debug``
Default: ``False``. If ``debug`` is ``True``, log messages to the
@@ -601,34 +580,10 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
http_only=False,
wild_domain=True,
debug=False,
- hashalg=_marker,
+ hashalg='sha512',
parent_domain=False,
domain=None,
):
- if hashalg is _marker:
- hashalg = 'md5'
- warnings.warn(
- 'The MD5 hash function used by default by the '
- 'AuthTktAuthenticationPolicy is known to be '
- 'susceptible to collision attacks. It is the current default '
- 'for backwards compatibility reasons, but we recommend that '
- 'you use the SHA512 algorithm instead for improved security. '
- 'Pass ``hashalg=\'sha512\'`` to the '
- 'AuthTktAuthenticationPolicy constructor to do so.\n\nNote '
- 'that a change to the hash algorithms will invalidate existing '
- 'auth tkt cookies set by your application. If backwards '
- 'compatibility of existing auth tkt cookies is of greater '
- 'concern than the risk posed by the potential for a hash '
- 'collision, you\'ll want to continue using MD5 explicitly. '
- 'To do so, pass ``hashalg=\'md5\'`` in your application to '
- 'the AuthTktAuthenticationPolicy constructor. When you do so '
- 'this warning will not be emitted again. The default '
- 'algorithm used in this policy will change in the future, so '
- 'setting an explicit hashalg will futureproof your '
- 'application.',
- DeprecationWarning,
- stacklevel=2
- )
self.cookie = AuthTktCookieHelper(
secret,
cookie_name=cookie_name,
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 5a1b7b122..553f32c9b 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -378,6 +378,7 @@ class Configurator(
self.add_default_response_adapters()
self.add_default_renderers()
self.add_default_view_predicates()
+ self.add_default_view_derivers()
self.add_default_route_predicates()
if exceptionresponse_view is not None:
@@ -521,10 +522,11 @@ class Configurator(
self.registry.registerUtility(predlist, IPredicateList, name=name)
return predlist
+
def _add_predicate(self, type, name, factory, weighs_more_than=None,
weighs_less_than=None):
factory = self.maybe_dotted(factory)
- discriminator = ('%s predicate' % type, name)
+ discriminator = ('%s option' % type, name)
intr = self.introspectable(
'%s predicates' % type,
discriminator,
diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py
index 967f2eeee..0b76bbd70 100644
--- a/pyramid/config/predicates.py
+++ b/pyramid/config/predicates.py
@@ -70,7 +70,12 @@ class RequestParamPredicate(object):
for p in val:
k = p
v = None
- if '=' in p:
+ if p.startswith('='):
+ if '=' in p[1:]:
+ k, v = p[1:].split('=', 1)
+ k = '=' + k
+ k, v = k.strip(), v.strip()
+ elif '=' in p:
k, v = p.split('=', 1)
k, v = k.strip(), v.strip()
reqs.append((k, v))
diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py
index 492b7d524..9e5c3b62d 100644
--- a/pyramid/config/settings.py
+++ b/pyramid/config/settings.py
@@ -122,6 +122,10 @@ class Settings(dict):
config_prevent_cachebust)
eff_prevent_cachebust = asbool(eget('PYRAMID_PREVENT_CACHEBUST',
config_prevent_cachebust))
+ require_default_csrf = self.get('pyramid.require_default_csrf')
+ eff_require_default_csrf = require_default_csrf
+ csrf_trusted_origins = self.get("pyramid.csrf_trusted_origins", [])
+ eff_csrf_trusted_origins = csrf_trusted_origins
update = {
'debug_authorization': eff_debug_all or eff_debug_auth,
@@ -134,6 +138,8 @@ class Settings(dict):
'default_locale_name':eff_locale_name,
'prevent_http_cache':eff_prevent_http_cache,
'prevent_cachebust':eff_prevent_cachebust,
+ 'require_default_csrf':eff_require_default_csrf,
+ 'csrf_trusted_origins':eff_csrf_trusted_origins,
'pyramid.debug_authorization': eff_debug_all or eff_debug_auth,
'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound,
@@ -145,7 +151,9 @@ class Settings(dict):
'pyramid.default_locale_name':eff_locale_name,
'pyramid.prevent_http_cache':eff_prevent_http_cache,
'pyramid.prevent_cachebust':eff_prevent_cachebust,
- }
+ 'pyramid.require_default_csrf':eff_require_default_csrf,
+ 'pyramid.csrf_trusted_origins':eff_csrf_trusted_origins,
+ }
self.update(update)
diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py
index cd14c9ff6..8e1800f33 100644
--- a/pyramid/config/tweens.py
+++ b/pyramid/config/tweens.py
@@ -18,6 +18,7 @@ from pyramid.tweens import (
from pyramid.config.util import (
action_method,
+ is_string_or_iterable,
TopologicalSorter,
)
@@ -122,12 +123,6 @@ class TweensConfiguratorMixin(object):
tween_factory = self.maybe_dotted(tween_factory)
- def is_string_or_iterable(v):
- if isinstance(v, string_types):
- return True
- if hasattr(v, '__iter__'):
- return True
-
for t, p in [('over', over), ('under', under)]:
if p is not None:
if not is_string_or_iterable(p):
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index 0fd9ef4a7..626e8d5fe 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -5,6 +5,7 @@ from pyramid.compat import (
bytes_,
getargspec,
is_nonstr_iter,
+ string_types,
)
from pyramid.compat import im_func
@@ -23,6 +24,12 @@ ActionInfo = ActionInfo # support bw compat imports
MAX_ORDER = 1 << 30
DEFAULT_PHASH = md5().hexdigest()
+def is_string_or_iterable(v):
+ if isinstance(v, string_types):
+ return True
+ if hasattr(v, '__iter__'):
+ return True
+
def as_sorted_tuple(val):
if not is_nonstr_iter(val):
val = (val,)
@@ -115,6 +122,10 @@ class PredicateList(object):
before=weighs_less_than,
)
+ def names(self):
+ # Return the list of valid predicate names.
+ return self.sorter.names
+
def make(self, config, **kw):
# Given a configurator and a list of keywords, a predicate list is
# computed. Elsewhere in the code, we evaluate predicates using a
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 20bcaa078..6fe31fd4a 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -8,15 +8,11 @@ from zope.interface import (
Interface,
implementedBy,
implementer,
- provider,
)
from zope.interface.interfaces import IInterface
from pyramid.interfaces import (
- IAuthenticationPolicy,
- IAuthorizationPolicy,
- IDebugLogger,
IDefaultPermission,
IException,
IExceptionViewClassifier,
@@ -30,7 +26,8 @@ from pyramid.interfaces import (
IStaticURLInfo,
IView,
IViewClassifier,
- IViewMapper,
+ IViewDerivers,
+ IViewDeriverInfo,
IViewMapperFactory,
PHASE1_CONFIG,
)
@@ -43,11 +40,11 @@ from pyramid.compat import (
urlparse,
url_quote,
WIN,
- is_bound_method,
- is_unbound_method,
is_nonstr_iter,
)
+from pyramid.decorator import reify
+
from pyramid.exceptions import (
ConfigurationError,
PredicateMismatch,
@@ -64,472 +61,46 @@ from pyramid.registry import (
Deferred,
)
-from pyramid.response import Response
-
from pyramid.security import NO_PERMISSION_REQUIRED
from pyramid.static import static_view
from pyramid.url import parse_url_overrides
-from pyramid.view import (
- render_view_to_response,
- AppendSlashNotFoundViewFactory,
- )
+from pyramid.view import AppendSlashNotFoundViewFactory
+import pyramid.util
from pyramid.util import (
- object_description,
viewdefaults,
action_method,
+ TopologicalSorter,
)
import pyramid.config.predicates
+import pyramid.viewderivers
+
+from pyramid.viewderivers import (
+ INGRESS,
+ VIEW,
+ preserve_view_attrs,
+ view_description,
+ requestonly,
+ DefaultViewMapper,
+ wraps_view,
+)
from pyramid.config.util import (
DEFAULT_PHASH,
MAX_ORDER,
- takes_one_arg,
+ as_sorted_tuple,
)
urljoin = urlparse.urljoin
url_parse = urlparse.urlparse
-def view_description(view):
- try:
- return view.__text__
- except AttributeError:
- # custom view mappers might not add __text__
- return object_description(view)
-
-def wraps_view(wrapper):
- def inner(self, view):
- wrapper_view = wrapper(self, view)
- return preserve_view_attrs(view, wrapper_view)
- return inner
-
-def preserve_view_attrs(view, wrapper):
- if view is None:
- return wrapper
-
- if wrapper is view:
- return view
-
- original_view = getattr(view, '__original_view__', None)
-
- if original_view is None:
- original_view = view
-
- wrapper.__wraps__ = view
- wrapper.__original_view__ = original_view
- wrapper.__module__ = view.__module__
- wrapper.__doc__ = view.__doc__
-
- try:
- wrapper.__name__ = view.__name__
- except AttributeError:
- wrapper.__name__ = repr(view)
-
- # attrs that may not exist on "view", but, if so, must be attached to
- # "wrapped view"
- for attr in ('__permitted__', '__call_permissive__', '__permission__',
- '__predicated__', '__predicates__', '__accept__',
- '__order__', '__text__'):
- try:
- setattr(wrapper, attr, getattr(view, attr))
- except AttributeError:
- pass
-
- return wrapper
-
-class ViewDeriver(object):
- def __init__(self, **kw):
- self.kw = kw
- self.registry = kw['registry']
- self.authn_policy = self.registry.queryUtility(IAuthenticationPolicy)
- self.authz_policy = self.registry.queryUtility(IAuthorizationPolicy)
- self.logger = self.registry.queryUtility(IDebugLogger)
-
- def __call__(self, view):
- return self.attr_wrapped_view(
- self.predicated_view(
- self.authdebug_view(
- self.secured_view(
- self.owrapped_view(
- self.http_cached_view(
- self.decorated_view(
- self.rendered_view(
- self.mapped_view(
- view)))))))))
-
- @wraps_view
- def mapped_view(self, view):
- mapper = self.kw.get('mapper')
- if mapper is None:
- mapper = getattr(view, '__view_mapper__', None)
- if mapper is None:
- mapper = self.registry.queryUtility(IViewMapperFactory)
- if mapper is None:
- mapper = DefaultViewMapper
-
- mapped_view = mapper(**self.kw)(view)
- return mapped_view
-
- @wraps_view
- def owrapped_view(self, view):
- wrapper_viewname = self.kw.get('wrapper_viewname')
- viewname = self.kw.get('viewname')
- if not wrapper_viewname:
- return view
- def _owrapped_view(context, request):
- response = view(context, request)
- request.wrapped_response = response
- request.wrapped_body = response.body
- request.wrapped_view = view
- wrapped_response = render_view_to_response(context, request,
- wrapper_viewname)
- if wrapped_response is None:
- raise ValueError(
- 'No wrapper view named %r found when executing view '
- 'named %r' % (wrapper_viewname, viewname))
- return wrapped_response
- return _owrapped_view
-
- @wraps_view
- def http_cached_view(self, view):
- if self.registry.settings.get('prevent_http_cache', False):
- return view
-
- seconds = self.kw.get('http_cache')
-
- if seconds is None:
- return view
-
- options = {}
-
- if isinstance(seconds, (tuple, list)):
- try:
- seconds, options = seconds
- except ValueError:
- raise ConfigurationError(
- 'If http_cache parameter is a tuple or list, it must be '
- 'in the form (seconds, options); not %s' % (seconds,))
-
- def wrapper(context, request):
- response = view(context, request)
- prevent_caching = getattr(response.cache_control, 'prevent_auto',
- False)
- if not prevent_caching:
- response.cache_expires(seconds, **options)
- return response
-
- return wrapper
-
- @wraps_view
- def secured_view(self, view):
- permission = self.kw.get('permission')
- if permission == NO_PERMISSION_REQUIRED:
- # allow views registered within configurations that have a
- # default permission to explicitly override the default
- # permission, replacing it with no permission at all
- permission = None
-
- wrapped_view = view
- if (
- self.authn_policy and
- self.authz_policy and
- (permission is not None)
- ):
- def _permitted(context, request):
- principals = self.authn_policy.effective_principals(request)
- return self.authz_policy.permits(context, principals,
- permission)
- def _secured_view(context, request):
- result = _permitted(context, request)
- if result:
- return view(context, request)
- view_name = getattr(view, '__name__', view)
- msg = getattr(
- request, 'authdebug_message',
- 'Unauthorized: %s failed permission check' % view_name)
- raise HTTPForbidden(msg, result=result)
- _secured_view.__call_permissive__ = view
- _secured_view.__permitted__ = _permitted
- _secured_view.__permission__ = permission
- wrapped_view = _secured_view
-
- return wrapped_view
-
- @wraps_view
- def authdebug_view(self, view):
- wrapped_view = view
- settings = self.registry.settings
- permission = self.kw.get('permission')
- if settings and settings.get('debug_authorization', False):
- def _authdebug_view(context, request):
- view_name = getattr(request, 'view_name', None)
-
- if self.authn_policy and self.authz_policy:
- if permission is NO_PERMISSION_REQUIRED:
- msg = 'Allowed (NO_PERMISSION_REQUIRED)'
- elif permission is None:
- msg = 'Allowed (no permission registered)'
- else:
- principals = self.authn_policy.effective_principals(
- request)
- msg = str(self.authz_policy.permits(context,
- principals,
- permission))
- else:
- msg = 'Allowed (no authorization policy in use)'
-
- view_name = getattr(request, 'view_name', None)
- url = getattr(request, 'url', None)
- msg = ('debug_authorization of url %s (view name %r against '
- 'context %r): %s' % (url, view_name, context, msg))
- self.logger and self.logger.debug(msg)
- if request is not None:
- request.authdebug_message = msg
- return view(context, request)
-
- wrapped_view = _authdebug_view
-
- return wrapped_view
-
- @wraps_view
- def predicated_view(self, view):
- preds = self.kw.get('predicates', ())
- if not preds:
- return view
-
- def predicate_wrapper(context, request):
- for predicate in preds:
- if not predicate(context, request):
- view_name = getattr(view, '__name__', view)
- raise PredicateMismatch(
- 'predicate mismatch for view %s (%s)' % (
- view_name, predicate.text()))
- return view(context, request)
-
- def checker(context, request):
- return all((predicate(context, request) for predicate in
- preds))
- predicate_wrapper.__predicated__ = checker
- predicate_wrapper.__predicates__ = preds
- return predicate_wrapper
-
- @wraps_view
- def attr_wrapped_view(self, view):
- kw = self.kw
- accept, order, phash = (kw.get('accept', None),
- kw.get('order', MAX_ORDER),
- kw.get('phash', DEFAULT_PHASH))
- # this is a little silly but we don't want to decorate the original
- # function with attributes that indicate accept, order, and phash,
- # so we use a wrapper
- if (
- (accept is None) and
- (order == MAX_ORDER) and
- (phash == DEFAULT_PHASH)
- ):
- return view # defaults
- def attr_view(context, request):
- return view(context, request)
- attr_view.__accept__ = accept
- attr_view.__order__ = order
- attr_view.__phash__ = phash
- attr_view.__view_attr__ = self.kw.get('attr')
- attr_view.__permission__ = self.kw.get('permission')
- return attr_view
-
- @wraps_view
- def rendered_view(self, view):
- # one way or another this wrapper must produce a Response (unless
- # the renderer is a NullRendererHelper)
- renderer = self.kw.get('renderer')
- if renderer is None:
- # register a default renderer if you want super-dynamic
- # rendering. registering a default renderer will also allow
- # override_renderer to work if a renderer is left unspecified for
- # a view registration.
- return self._response_resolved_view(view)
- if renderer is renderers.null_renderer:
- return view
- return self._rendered_view(view, renderer)
-
- def _rendered_view(self, view, view_renderer):
- def rendered_view(context, request):
- result = view(context, request)
- if result.__class__ is Response: # potential common case
- response = result
- else:
- registry = self.registry
- # this must adapt, it can't do a simple interface check
- # (avoid trying to render webob responses)
- response = registry.queryAdapterOrSelf(result, IResponse)
- if response is None:
- attrs = getattr(request, '__dict__', {})
- if 'override_renderer' in attrs:
- # renderer overridden by newrequest event or other
- renderer_name = attrs.pop('override_renderer')
- renderer = renderers.RendererHelper(
- name=renderer_name,
- package=self.kw.get('package'),
- registry=registry)
- else:
- renderer = view_renderer.clone()
-
- if '__view__' in attrs:
- view_inst = attrs.pop('__view__')
- else:
- view_inst = getattr(view, '__original_view__', view)
- response = renderer.render_view(request, result, view_inst,
- context)
- return response
-
- return rendered_view
-
- def _response_resolved_view(self, view):
- registry = self.registry
-
- def viewresult_to_response(context, request):
- result = view(context, request)
- if result.__class__ is Response: # common case
- response = result
- else:
- response = registry.queryAdapterOrSelf(result, IResponse)
- if response is None:
- if result is None:
- append = (' You may have forgotten to return a value '
- 'from the view callable.')
- elif isinstance(result, dict):
- append = (' You may have forgotten to define a '
- 'renderer in the view configuration.')
- else:
- append = ''
-
- msg = ('Could not convert return value of the view '
- 'callable %s into a response object. '
- 'The value returned was %r.' + append)
-
- raise ValueError(msg % (view_description(view), result))
-
- return response
-
- return viewresult_to_response
-
- @wraps_view
- def decorated_view(self, view):
- decorator = self.kw.get('decorator')
- if decorator is None:
- return view
- return decorator(view)
-
-
-@implementer(IViewMapper)
-@provider(IViewMapperFactory)
-class DefaultViewMapper(object):
- def __init__(self, **kw):
- self.attr = kw.get('attr')
-
- def __call__(self, view):
- if is_unbound_method(view) and self.attr is None:
- raise ConfigurationError((
- 'Unbound method calls are not supported, please set the class '
- 'as your `view` and the method as your `attr`'
- ))
-
- if inspect.isclass(view):
- view = self.map_class(view)
- else:
- view = self.map_nonclass(view)
- return view
-
- def map_class(self, view):
- ronly = requestonly(view, self.attr)
- if ronly:
- mapped_view = self.map_class_requestonly(view)
- else:
- mapped_view = self.map_class_native(view)
- mapped_view.__text__ = 'method %s of %s' % (
- self.attr or '__call__', object_description(view))
- return mapped_view
-
- def map_nonclass(self, view):
- # We do more work here than appears necessary to avoid wrapping the
- # view unless it actually requires wrapping (to avoid function call
- # overhead).
- mapped_view = view
- ronly = requestonly(view, self.attr)
- if ronly:
- mapped_view = self.map_nonclass_requestonly(view)
- elif self.attr:
- mapped_view = self.map_nonclass_attr(view)
- if inspect.isroutine(mapped_view):
- # This branch will be true if the view is a function or a method.
- # We potentially mutate an unwrapped object here if it's a
- # function. We do this to avoid function call overhead of
- # injecting another wrapper. However, we must wrap if the
- # function is a bound method because we can't set attributes on a
- # bound method.
- if is_bound_method(view):
- _mapped_view = mapped_view
- def mapped_view(context, request):
- return _mapped_view(context, request)
- if self.attr is not None:
- mapped_view.__text__ = 'attr %s of %s' % (
- self.attr, object_description(view))
- else:
- mapped_view.__text__ = object_description(view)
- return mapped_view
-
- def map_class_requestonly(self, view):
- # its a class that has an __init__ which only accepts request
- attr = self.attr
- def _class_requestonly_view(context, request):
- inst = view(request)
- request.__view__ = inst
- if attr is None:
- response = inst()
- else:
- response = getattr(inst, attr)()
- return response
- return _class_requestonly_view
-
- def map_class_native(self, view):
- # its a class that has an __init__ which accepts both context and
- # request
- attr = self.attr
- def _class_view(context, request):
- inst = view(context, request)
- request.__view__ = inst
- if attr is None:
- response = inst()
- else:
- response = getattr(inst, attr)()
- return response
- return _class_view
-
- def map_nonclass_requestonly(self, view):
- # its a function that has a __call__ which accepts only a single
- # request argument
- attr = self.attr
- def _requestonly_view(context, request):
- if attr is None:
- response = view(request)
- else:
- response = getattr(view, attr)(request)
- return response
- return _requestonly_view
-
- def map_nonclass_attr(self, view):
- # its a function that has a __call__ which accepts both context and
- # request, but still has an attr
- def _attr_view(context, request):
- response = getattr(view, self.attr)(context, request)
- return response
- return _attr_view
-
-def requestonly(view, attr=None):
- return takes_one_arg(view, attr=attr, argname='request')
+DefaultViewMapper = DefaultViewMapper # bw-compat
+preserve_view_attrs = preserve_view_attrs # bw-compat
+requestonly = requestonly # bw-compat
+view_description = view_description # bw-compat
@implementer(IMultiView)
class MultiView(object):
@@ -642,8 +213,8 @@ class ViewsConfiguratorMixin(object):
http_cache=None,
match_param=None,
check_csrf=None,
- **predicates
- ):
+ require_csrf=None,
+ **view_options):
""" Add a :term:`view configuration` to the current
configuration state. Arguments to ``add_view`` are broken
down below into *predicate* arguments and *non-predicate*
@@ -796,6 +367,31 @@ class ViewsConfiguratorMixin(object):
before returning the response from the view. This effectively
disables any HTTP caching done by ``http_cache`` for that response.
+ require_csrf
+
+ .. versionadded:: 1.7
+
+ CSRF checks only affect POST requests. Any other request methods
+ will pass untouched. This option is used in combination with the
+ ``pyramid.require_default_csrf`` setting to control which
+ request parameters are checked for CSRF tokens.
+
+ This feature requires a configured :term:`session factory`.
+
+ If this option is set to ``True`` then CSRF checks will be enabled
+ for POST requests to this view. The required token will be whatever
+ was specified by the ``pyramid.require_default_csrf`` setting, or
+ will fallback to ``csrf_token``.
+
+ If this option is set to a string then CSRF checks will be enabled
+ and it will be used as the required token regardless of the
+ ``pyramid.require_default_csrf`` setting.
+
+ If this option is set to ``False`` then CSRF checks will be disabled
+ regardless of the ``pyramid.require_default_csrf`` setting.
+
+ See :ref:`auto_csrf_checking` for more information.
+
wrapper
The :term:`view name` of a different :term:`view
@@ -1017,6 +613,11 @@ class ViewsConfiguratorMixin(object):
check_csrf
+ .. deprecated:: 1.7
+ Use the ``require_csrf`` option or see :ref:`auto_csrf_checking`
+ instead to have :class:`pyramid.exceptions.BadCSRFToken`
+ exceptions raised.
+
If specified, this value should be one of ``None``, ``True``,
``False``, or a string representing the 'check name'. If the value
is ``True`` or a string, CSRF checking will be performed. If the
@@ -1084,17 +685,23 @@ class ViewsConfiguratorMixin(object):
obsoletes this argument, but it is kept around for backwards
compatibility.
- predicates
+ view_options:
- Pass a key/value pair here to use a third-party predicate
- registered via
- :meth:`pyramid.config.Configurator.add_view_predicate`. More than
- one key/value pair can be used at the same time. See
+ Pass a key/value pair here to use a third-party predicate or set a
+ value for a view deriver. See
+ :meth:`pyramid.config.Configurator.add_view_predicate` and
+ :meth:`pyramid.config.Configurator.add_view_deriver`. See
:ref:`view_and_route_predicates` for more information about
- third-party predicates.
+ third-party predicates and :ref:`view_derivers` for information
+ about view derivers.
.. versionadded: 1.4a1
+ .. versionchanged: 1.7
+
+ Support setting view deriver options. Previously, only custom
+ view predicate values could be supplied.
+
"""
if custom_predicates:
warnings.warn(
@@ -1106,7 +713,18 @@ class ViewsConfiguratorMixin(object):
'Predicate" in the "Hooks" chapter of the documentation '
'for more information.'),
DeprecationWarning,
- stacklevel=4
+ stacklevel=4,
+ )
+
+ if check_csrf is not None:
+ warnings.warn(
+ ('The "check_csrf" argument to Configurator.add_view is '
+ 'deprecated as of Pyramid 1.7. Use the "require_csrf" option '
+ 'instead or see "Checking CSRF Tokens Automatically" in the '
+ '"Sessions" chapter of the documentation for more '
+ 'information.'),
+ DeprecationWarning,
+ stacklevel=4,
)
view = self.maybe_dotted(view)
@@ -1117,7 +735,7 @@ class ViewsConfiguratorMixin(object):
def combine(*decorators):
def decorated(view_callable):
- # reversed() is allows a more natural ordering in the api
+ # reversed() allows a more natural ordering in the api
for decorator in reversed(decorators):
view_callable = decorator(view_callable)
return view_callable
@@ -1160,33 +778,45 @@ class ViewsConfiguratorMixin(object):
accept = accept.lower()
introspectables = []
- pvals = predicates.copy()
- pvals.update(
- dict(
- xhr=xhr,
- request_method=request_method,
- path_info=path_info,
- request_param=request_param,
- header=header,
- accept=accept,
- containment=containment,
- request_type=request_type,
- match_param=match_param,
- check_csrf=check_csrf,
- custom=predvalseq(custom_predicates),
- )
- )
+ ovals = view_options.copy()
+ ovals.update(dict(
+ xhr=xhr,
+ request_method=request_method,
+ path_info=path_info,
+ request_param=request_param,
+ header=header,
+ accept=accept,
+ containment=containment,
+ request_type=request_type,
+ match_param=match_param,
+ check_csrf=check_csrf,
+ custom=predvalseq(custom_predicates),
+ ))
def discrim_func():
# We need to defer the discriminator until we know what the phash
# is. It can't be computed any sooner because thirdparty
- # predicates may not yet exist when add_view is called.
+ # predicates/view derivers may not yet exist when add_view is
+ # called.
+ valid_predicates = predlist.names()
+ pvals = {}
+ dvals = {}
+
+ for (k, v) in ovals.items():
+ if k in valid_predicates:
+ pvals[k] = v
+ else:
+ dvals[k] = v
+
+ self._check_view_options(**dvals)
+
order, preds, phash = predlist.make(self, **pvals)
+
view_intr.update({
'phash': phash,
'order': order,
- 'predicates': preds
- })
+ 'predicates': preds,
+ })
return ('view', context, name, route_name, phash)
discriminator = Deferred(discrim_func)
@@ -1203,26 +833,27 @@ class ViewsConfiguratorMixin(object):
discriminator,
view_desc,
'view')
- view_intr.update(
- dict(name=name,
- context=context,
- containment=containment,
- request_param=request_param,
- request_methods=request_method,
- route_name=route_name,
- attr=attr,
- xhr=xhr,
- accept=accept,
- header=header,
- path_info=path_info,
- match_param=match_param,
- check_csrf=check_csrf,
- callable=view,
- mapper=mapper,
- decorator=decorator,
- )
- )
- view_intr.update(**predicates)
+ view_intr.update(dict(
+ name=name,
+ context=context,
+ containment=containment,
+ request_param=request_param,
+ request_methods=request_method,
+ route_name=route_name,
+ attr=attr,
+ xhr=xhr,
+ accept=accept,
+ header=header,
+ path_info=path_info,
+ match_param=match_param,
+ check_csrf=check_csrf,
+ http_cache=http_cache,
+ require_csrf=require_csrf,
+ callable=view,
+ mapper=mapper,
+ decorator=decorator,
+ ))
+ view_intr.update(view_options)
introspectables.append(view_intr)
predlist = self.get_predlist('view')
@@ -1258,23 +889,24 @@ class ViewsConfiguratorMixin(object):
phash = view_intr['phash']
# __no_permission_required__ handled by _secure_view
- deriver = ViewDeriver(
- registry=self.registry,
+ derived_view = self._derive_view(
+ view,
permission=permission,
predicates=preds,
attr=attr,
+ context=context,
renderer=renderer,
wrapper_viewname=wrapper,
viewname=name,
accept=accept,
order=order,
phash=phash,
- package=self.package,
- mapper=mapper,
decorator=decorator,
+ mapper=mapper,
http_cache=http_cache,
- )
- derived_view = deriver(view)
+ require_csrf=require_csrf,
+ extra_options=ovals,
+ )
derived_view.__discriminator__ = lambda *arg: discriminator
# __discriminator__ is used by superdynamic systems
# that require it for introspection after manual view lookup;
@@ -1385,7 +1017,7 @@ class ViewsConfiguratorMixin(object):
tmpl_intr is not None and
intrspc is not None and
intrspc.get('renderer factories', renderer_type) is not None
- ):
+ ):
# allow failure of registered template factories to be deferred
# until view execution, like other bad renderer factories; if
# we tried to relate this to an existing renderer factory
@@ -1427,12 +1059,35 @@ class ViewsConfiguratorMixin(object):
permission,
permission,
'permission'
- )
+ )
perm_intr['value'] = permission
perm_intr.relate('views', discriminator)
introspectables.append(perm_intr)
self.action(discriminator, register, introspectables=introspectables)
+ def _check_view_options(self, **kw):
+ # we only need to validate deriver options because the predicates
+ # were checked by the predlist
+ derivers = self.registry.getUtility(IViewDerivers)
+ for deriver in derivers.values():
+ for opt in getattr(deriver, 'options', []):
+ kw.pop(opt, None)
+ if kw:
+ raise ConfigurationError('Unknown view options: %s' % (kw,))
+
+ def _apply_view_derivers(self, info):
+ d = pyramid.viewderivers
+
+ # These derivers are not really derivers and so have fixed order
+ outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view),
+ ('predicated_view', d.predicated_view)]
+
+ view = info.original_view
+ derivers = self.registry.getUtility(IViewDerivers)
+ for name, deriver in reversed(outer_derivers + derivers.sorted()):
+ view = wraps_view(deriver)(view, info)
+ return view
+
@action_method
def add_view_predicate(self, name, factory, weighs_more_than=None,
weighs_less_than=None):
@@ -1459,7 +1114,7 @@ class ViewsConfiguratorMixin(object):
factory,
weighs_more_than=weighs_more_than,
weighs_less_than=weighs_less_than
- )
+ )
def add_default_view_predicates(self):
p = pyramid.config.predicates
@@ -1477,9 +1132,120 @@ class ViewsConfiguratorMixin(object):
('physical_path', p.PhysicalPathPredicate),
('effective_principals', p.EffectivePrincipalsPredicate),
('custom', p.CustomPredicate),
- ):
+ ):
self.add_view_predicate(name, factory)
+ @action_method
+ def add_view_deriver(self, deriver, name=None, under=None, over=None):
+ """
+ .. versionadded:: 1.7
+
+ Add a :term:`view deriver` to the view pipeline. View derivers are
+ a feature used by extension authors to wrap views in custom code
+ controllable by view-specific options.
+
+ ``deriver`` should be a callable conforming to the
+ :class:`pyramid.interfaces.IViewDeriver` interface.
+
+ ``name`` should be the name of the view deriver. There are no
+ restrictions on the name of a view deriver. If left unspecified, the
+ name will be constructed from the name of the ``deriver``.
+
+ The ``under`` and ``over`` options can be used to control the ordering
+ of view derivers by providing hints about where in the view pipeline
+ the deriver is used. Each option may be a string or a list of strings.
+ At least one view deriver in each, the over and under directions, must
+ exist to fully satisfy the constraints.
+
+ ``under`` means closer to the user-defined :term:`view callable`,
+ and ``over`` means closer to view pipeline ingress.
+
+ The default value for ``over`` is ``rendered_view`` and ``under`` is
+ ``decorated_view``. This places the deriver somewhere between the two
+ in the view pipeline. If the deriver should be placed elsewhere in the
+ pipeline, such as above ``decorated_view``, then you MUST also specify
+ ``under`` to something earlier in the order, or a
+ ``CyclicDependencyError`` will be raised when trying to sort the
+ derivers.
+
+ See :ref:`view_derivers` for more information.
+
+ """
+ deriver = self.maybe_dotted(deriver)
+
+ if name is None:
+ name = deriver.__name__
+
+ if name in (INGRESS, VIEW):
+ raise ConfigurationError('%s is a reserved view deriver name'
+ % name)
+
+ if under is None:
+ under = 'decorated_view'
+
+ if over is None:
+ over = 'rendered_view'
+
+ over = as_sorted_tuple(over)
+ under = as_sorted_tuple(under)
+
+ if INGRESS in over:
+ raise ConfigurationError('%s cannot be over INGRESS' % name)
+
+ # ensure everything is always over mapped_view
+ if VIEW in over and name != 'mapped_view':
+ over = as_sorted_tuple(over + ('mapped_view',))
+
+ if VIEW in under:
+ raise ConfigurationError('%s cannot be under VIEW' % name)
+ if 'mapped_view' in under:
+ raise ConfigurationError('%s cannot be under "mapped_view"' % name)
+
+ discriminator = ('view deriver', name)
+ intr = self.introspectable(
+ 'view derivers',
+ name,
+ name,
+ 'view deriver')
+ intr['name'] = name
+ intr['deriver'] = deriver
+ intr['under'] = under
+ intr['over'] = over
+ def register():
+ derivers = self.registry.queryUtility(IViewDerivers)
+ if derivers is None:
+ derivers = TopologicalSorter(
+ default_before=None,
+ default_after=INGRESS,
+ first=INGRESS,
+ last=VIEW,
+ )
+ self.registry.registerUtility(derivers, IViewDerivers)
+ derivers.add(name, deriver, before=over, after=under)
+ self.action(discriminator, register, introspectables=(intr,),
+ order=PHASE1_CONFIG) # must be registered before add_view
+
+ def add_default_view_derivers(self):
+ d = pyramid.viewderivers
+ derivers = [
+ ('secured_view', d.secured_view),
+ ('csrf_view', d.csrf_view),
+ ('owrapped_view', d.owrapped_view),
+ ('http_cached_view', d.http_cached_view),
+ ('decorated_view', d.decorated_view),
+ ('rendered_view', d.rendered_view),
+ ('mapped_view', d.mapped_view),
+ ]
+ last = INGRESS
+ for name, deriver in derivers:
+ self.add_view_deriver(
+ deriver,
+ name=name,
+ under=last,
+ over=VIEW,
+ )
+ last = name
+
def derive_view(self, view, attr=None, renderer=None):
"""
Create a :term:`view callable` using the function, instance,
@@ -1563,7 +1329,8 @@ class ViewsConfiguratorMixin(object):
attr=None, renderer=None, wrapper_viewname=None,
viewname=None, accept=None, order=MAX_ORDER,
phash=DEFAULT_PHASH, decorator=None,
- mapper=None, http_cache=None):
+ mapper=None, http_cache=None, context=None,
+ require_csrf=None, extra_options=None):
view = self.maybe_dotted(view)
mapper = self.maybe_dotted(mapper)
if isinstance(renderer, string_types):
@@ -1578,22 +1345,37 @@ class ViewsConfiguratorMixin(object):
package=self.package,
registry=self.registry)
- deriver = ViewDeriver(registry=self.registry,
- permission=permission,
- predicates=predicates,
- attr=attr,
- renderer=renderer,
- wrapper_viewname=wrapper_viewname,
- viewname=viewname,
- accept=accept,
- order=order,
- phash=phash,
- package=self.package,
- mapper=mapper,
- decorator=decorator,
- http_cache=http_cache)
-
- return deriver(view)
+ options = dict(
+ view=view,
+ context=context,
+ permission=permission,
+ attr=attr,
+ renderer=renderer,
+ wrapper=wrapper_viewname,
+ name=viewname,
+ accept=accept,
+ mapper=mapper,
+ decorator=decorator,
+ http_cache=http_cache,
+ require_csrf=require_csrf,
+ )
+ if extra_options:
+ options.update(extra_options)
+
+ info = ViewDeriverInfo(
+ view=view,
+ registry=self.registry,
+ package=self.package,
+ predicates=predicates,
+ options=options,
+ )
+
+ # order and phash are only necessary for the predicated view and
+ # are not really view deriver options
+ info.order = order
+ info.phash = phash
+
+ return self._apply_view_derivers(info)
@viewdefaults
@action_method
@@ -1616,8 +1398,8 @@ class ViewsConfiguratorMixin(object):
decorator=None,
mapper=None,
match_param=None,
- **predicates
- ):
+ **view_options
+ ):
""" Add a forbidden view to the current configuration state. The
view will be called when Pyramid or application code raises a
:exc:`pyramid.httpexceptions.HTTPForbidden` exception and the set of
@@ -1646,11 +1428,11 @@ class ViewsConfiguratorMixin(object):
.. versionadded:: 1.3
"""
for arg in ('name', 'permission', 'context', 'for_', 'http_cache'):
- if arg in predicates:
+ if arg in view_options:
raise ConfigurationError(
'%s may not be used as an argument to add_forbidden_view'
% arg
- )
+ )
if view is None:
view = default_exceptionresponse_view
@@ -1675,8 +1457,8 @@ class ViewsConfiguratorMixin(object):
permission=NO_PERMISSION_REQUIRED,
attr=attr,
renderer=renderer,
- )
- settings.update(predicates)
+ )
+ settings.update(view_options)
return self.add_view(**settings)
set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias
@@ -1703,8 +1485,8 @@ class ViewsConfiguratorMixin(object):
mapper=None,
match_param=None,
append_slash=False,
- **predicates
- ):
+ **view_options
+ ):
""" Add a default Not Found View to the current configuration state.
The view will be called when Pyramid or application code raises an
:exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g. when a
@@ -1758,11 +1540,11 @@ class ViewsConfiguratorMixin(object):
.. versionadded:: 1.3
"""
for arg in ('name', 'permission', 'context', 'for_', 'http_cache'):
- if arg in predicates:
+ if arg in view_options:
raise ConfigurationError(
'%s may not be used as an argument to add_notfound_view'
% arg
- )
+ )
if view is None:
view = default_exceptionresponse_view
@@ -1785,8 +1567,8 @@ class ViewsConfiguratorMixin(object):
match_param=match_param,
route_name=route_name,
permission=NO_PERMISSION_REQUIRED,
- )
- settings.update(predicates)
+ )
+ settings.update(view_options)
if append_slash:
view = self._derive_view(view, attr=attr, renderer=renderer)
if IResponse.implementedBy(append_slash):
@@ -1980,8 +1762,20 @@ def isexception(o):
return (
isinstance(o, Exception) or
(inspect.isclass(o) and (issubclass(o, Exception)))
- )
+ )
+
+@implementer(IViewDeriverInfo)
+class ViewDeriverInfo(object):
+ def __init__(self, view, registry, package, predicates, options):
+ self.original_view = view
+ self.registry = registry
+ self.package = package
+ self.predicates = predicates or []
+ self.options = options or {}
+ @reify
+ def settings(self):
+ return self.registry.settings
@implementer(IStaticURLInfo)
class StaticURLInfo(object):
@@ -2066,7 +1860,7 @@ class StaticURLInfo(object):
# register a route using the computed view, permission, and
# pattern, plus any extras passed to us via add_static_view
- pattern = "%s*subpath" % name # name already ends with slash
+ pattern = "%s*subpath" % name # name already ends with slash
if config.route_prefix:
route_name = '__%s/%s' % (config.route_prefix, name)
else:
diff --git a/pyramid/events.py b/pyramid/events.py
index 97375638e..35da2fa6f 100644
--- a/pyramid/events.py
+++ b/pyramid/events.py
@@ -11,6 +11,7 @@ from pyramid.interfaces import (
INewResponse,
IApplicationCreated,
IBeforeRender,
+ IBeforeTraversal,
)
class subscriber(object):
@@ -129,6 +130,26 @@ class NewResponse(object):
self.request = request
self.response = response
+@implementer(IBeforeTraversal)
+class BeforeTraversal(object):
+ """
+ An instance of this class is emitted as an :term:`event` after the
+ :app:`Pyramid` :term:`router` has attempted to find a :term:`route` object
+ but before any traversal or view code is executed. The instance has an
+ attribute, ``request``, which is the request object generated by
+ :app:`Pyramid`.
+
+ Notably, the request object **may** have an attribute named
+ ``matched_route``, which is the matched route if found. If no route
+ matched, this attribute is not available.
+
+ This class implements the :class:`pyramid.interfaces.IBeforeTraversal`
+ interface.
+ """
+
+ def __init__(self, request):
+ self.request = request
+
@implementer(IContextFound)
class ContextFound(object):
""" An instance of this class is emitted as an :term:`event` after
@@ -156,7 +177,7 @@ class ContextFound(object):
AfterTraversal = ContextFound # b/c as of 1.0
@implementer(IApplicationCreated)
-class ApplicationCreated(object):
+class ApplicationCreated(object):
""" An instance of this class is emitted as an :term:`event` when
the :meth:`pyramid.config.Configurator.make_wsgi_app` is
called. The instance has an attribute, ``app``, which is an
@@ -243,4 +264,3 @@ class BeforeRender(dict):
dict.__init__(self, system)
self.rendering_val = rendering_val
-
diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py
index c1481ce9c..a8a10f927 100644
--- a/pyramid/exceptions.py
+++ b/pyramid/exceptions.py
@@ -9,6 +9,21 @@ Forbidden = HTTPForbidden # bw compat
CR = '\n'
+
+class BadCSRFOrigin(HTTPBadRequest):
+ """
+ This exception indicates the request has failed cross-site request forgery
+ origin validation.
+ """
+ title = "Bad CSRF Origin"
+ explanation = (
+ "Access is denied. This server can not verify that the origin or "
+ "referrer of your request matches the current site. Either your "
+ "browser supplied the wrong Origin or Referrer or it did not supply "
+ "one at all."
+ )
+
+
class BadCSRFToken(HTTPBadRequest):
"""
This exception indicates the request has failed cross-site request
diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py
index 8bf9a0a72..e76f43c8a 100644
--- a/pyramid/httpexceptions.py
+++ b/pyramid/httpexceptions.py
@@ -123,12 +123,14 @@ The subclasses of :class:`~_HTTPMove`
field. Reflecting this, these subclasses have one additional keyword argument:
``location``, which indicates the location to which to redirect.
"""
+import json
from string import Template
from zope.interface import implementer
from webob import html_escape as _html_escape
+from webob.acceptparse import MIMEAccept
from pyramid.compat import (
class_types,
@@ -214,7 +216,7 @@ ${body}''')
empty_body = False
def __init__(self, detail=None, headers=None, comment=None,
- body_template=None, **kw):
+ body_template=None, json_formatter=None, **kw):
status = '%s %s' % (self.code, self.title)
Response.__init__(self, status=status, **kw)
Exception.__init__(self, detail)
@@ -225,6 +227,8 @@ ${body}''')
if body_template is not None:
self.body_template = body_template
self.body_template_obj = Template(body_template)
+ if json_formatter is not None:
+ self._json_formatter = json_formatter
if self.empty_body:
del self.content_type
@@ -233,18 +237,48 @@ ${body}''')
def __str__(self):
return self.detail or self.explanation
+ def _json_formatter(self, status, body, title, environ):
+ return {'message': body,
+ 'code': status,
+ 'title': self.title}
+
def prepare(self, environ):
if not self.body and not self.empty_body:
html_comment = ''
comment = self.comment or ''
- accept = environ.get('HTTP_ACCEPT', '')
- if accept and 'html' in accept or '*/*' in accept:
+ accept_value = environ.get('HTTP_ACCEPT', '')
+ accept = MIMEAccept(accept_value)
+ # Attempt to match text/html or application/json, if those don't
+ # match, we will fall through to defaulting to text/plain
+ match = accept.best_match(['text/html', 'application/json'])
+
+ if match == 'text/html':
self.content_type = 'text/html'
escape = _html_escape
page_template = self.html_template_obj
br = '<br/>'
if comment:
html_comment = '<!-- %s -->' % escape(comment)
+ elif match == 'application/json':
+ self.content_type = 'application/json'
+ self.charset = None
+ escape = _no_escape
+ br = '\n'
+ if comment:
+ html_comment = escape(comment)
+
+ class JsonPageTemplate(object):
+ def __init__(self, excobj):
+ self.excobj = excobj
+
+ def substitute(self, status, body):
+ jsonbody = self.excobj._json_formatter(
+ status=status,
+ body=body, title=self.excobj.title,
+ environ=environ)
+ return json.dumps(jsonbody)
+
+ page_template = JsonPageTemplate(self)
else:
self.content_type = 'text/plain'
escape = _no_escape
@@ -253,11 +287,11 @@ ${body}''')
if comment:
html_comment = escape(comment)
args = {
- 'br':br,
+ 'br': br,
'explanation': escape(self.explanation),
'detail': escape(self.detail or ''),
'comment': escape(comment),
- 'html_comment':html_comment,
+ 'html_comment': html_comment,
}
body_tmpl = self.body_template_obj
if HTTPException.body_template_obj is not body_tmpl:
@@ -274,7 +308,7 @@ ${body}''')
body = body_tmpl.substitute(args)
page = page_template.substitute(status=self.status, body=body)
if isinstance(page, text_type):
- page = page.encode(self.charset)
+ page = page.encode(self.charset if self.charset else 'UTF-8')
self.app_iter = [page]
self.body = page
@@ -1001,8 +1035,8 @@ class HTTPInternalServerError(HTTPServerError):
code = 500
title = 'Internal Server Error'
explanation = (
- 'The server has either erred or is incapable of performing '
- 'the requested operation.')
+ 'The server has either erred or is incapable of performing '
+ 'the requested operation.')
class HTTPNotImplemented(HTTPServerError):
"""
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index 9e5cbb6d3..2b00752cf 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -25,6 +25,14 @@ class IContextFound(Interface):
IAfterTraversal = IContextFound
+class IBeforeTraversal(Interface):
+ """
+ An event type that is emitted after :app:`Pyramid` attempted to find a
+ route but before it calls any traversal or view code. See the documentation
+ attached to :class:`pyramid.events.Routefound` for more information.
+ """
+ request = Attribute('The request object')
+
class INewRequest(Interface):
""" An event type that is emitted whenever :app:`Pyramid`
begins to process a new request. See the documentation attached
@@ -1187,6 +1195,39 @@ class IJSONAdapter(Interface):
class IPredicateList(Interface):
""" Interface representing a predicate list """
+class IViewDeriver(Interface):
+ options = Attribute('A list of supported options to be passed to '
+ ':meth:`pyramid.config.Configurator.add_view`. '
+ 'This attribute is optional.')
+
+ def __call__(view, info):
+ """
+ Derive a new view from the supplied view.
+
+ View options, package information and registry are available on
+ ``info``, an instance of :class:`pyramid.interfaces.IViewDeriverInfo`.
+
+ The ``view`` is a callable accepting ``(context, request)``.
+
+ """
+
+class IViewDeriverInfo(Interface):
+ """ An object implementing this interface is passed to every
+ :term:`view deriver` during configuration."""
+ registry = Attribute('The "current" application registry when the '
+ 'view was created')
+ package = Attribute('The "current package" when the view '
+ 'configuration statement was found')
+ settings = Attribute('The deployment settings dictionary related '
+ 'to the current application')
+ options = Attribute('The view options passed to the view, including any '
+ 'default values that were not overriden')
+ predicates = Attribute('The list of predicates active on the view')
+ original_view = Attribute('The original view object being wrapped')
+
+class IViewDerivers(Interface):
+ """ Interface for view derivers list """
+
class ICacheBuster(Interface):
"""
A cache buster modifies the URL generation machinery for
diff --git a/pyramid/paster.py b/pyramid/paster.py
index 967543849..3916be8f0 100644
--- a/pyramid/paster.py
+++ b/pyramid/paster.py
@@ -52,25 +52,29 @@ def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig):
relative_to=here_dir,
global_conf=options)
-def setup_logging(config_uri, fileConfig=fileConfig,
+def setup_logging(config_uri, global_conf=None,
+ fileConfig=fileConfig,
configparser=configparser):
"""
- Set up logging via the logging module's fileConfig function with the
- filename specified via ``config_uri`` (a string in the form
+ Set up logging via :func:`logging.config.fileConfig` with the filename
+ specified via ``config_uri`` (a string in the form
``filename#sectionname``).
ConfigParser defaults are specified for the special ``__file__``
and ``here`` variables, similar to PasteDeploy config loading.
+ Extra defaults can optionally be specified as a dict in ``global_conf``.
"""
path, _ = _getpathsec(config_uri, None)
parser = configparser.ConfigParser()
parser.read([path])
if parser.has_section('loggers'):
config_file = os.path.abspath(path)
- return fileConfig(
- config_file,
- dict(__file__=config_file, here=os.path.dirname(config_file))
- )
+ full_global_conf = dict(
+ __file__=config_file,
+ here=os.path.dirname(config_file))
+ if global_conf:
+ full_global_conf.update(global_conf)
+ return fileConfig(config_file, full_global_conf)
def _getpathsec(config_uri, name):
if '#' in config_uri:
diff --git a/pyramid/renderers.py b/pyramid/renderers.py
index 456b16c82..bcbcbb0aa 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -1,4 +1,3 @@
-import contextlib
import json
import os
import re
@@ -30,6 +29,7 @@ from pyramid.path import caller_package
from pyramid.response import _get_response_factory
from pyramid.threadlocal import get_current_registry
+from pyramid.util import hide_attrs
# API
@@ -77,7 +77,7 @@ def render(renderer_name, value, request=None, package=None):
helper = RendererHelper(name=renderer_name, package=package,
registry=registry)
- with temporary_response(request):
+ with hide_attrs(request, 'response'):
result = helper.render(value, None, request=request)
return result
@@ -138,30 +138,13 @@ def render_to_response(renderer_name,
helper = RendererHelper(name=renderer_name, package=package,
registry=registry)
- with temporary_response(request):
+ with hide_attrs(request, 'response'):
if response is not None:
request.response = response
result = helper.render_to_response(value, None, request=request)
return result
-_marker = object()
-
-@contextlib.contextmanager
-def temporary_response(request):
- """
- Temporarily delete request.response and restore it afterward.
- """
- attrs = request.__dict__ if request is not None else {}
- saved_response = attrs.pop('response', _marker)
- try:
- yield
- finally:
- if saved_response is not _marker:
- attrs['response'] = saved_response
- elif 'response' in attrs:
- del attrs['response']
-
def get_renderer(renderer_name, package=None):
""" Return the renderer object for the renderer ``renderer_name``.
diff --git a/pyramid/request.py b/pyramid/request.py
index 45d936cef..c1c1da514 100644
--- a/pyramid/request.py
+++ b/pyramid/request.py
@@ -32,6 +32,7 @@ from pyramid.util import (
InstancePropertyHelper,
InstancePropertyMixin,
)
+from pyramid.view import ViewMethodsMixin
class TemplateContext(object):
pass
@@ -154,6 +155,7 @@ class Request(
LocalizerRequestMixin,
AuthenticationAPIMixin,
AuthorizationAPIMixin,
+ ViewMethodsMixin,
):
"""
A subclass of the :term:`WebOb` Request class. An instance of
diff --git a/pyramid/router.py b/pyramid/router.py
index 4054ef52e..19773cf62 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -20,6 +20,7 @@ from pyramid.events import (
ContextFound,
NewRequest,
NewResponse,
+ BeforeTraversal,
)
from pyramid.httpexceptions import HTTPNotFound
@@ -114,10 +115,19 @@ class Router(object):
root_factory = route.factory or self.root_factory
+ # Notify anyone listening that we are about to start traversal
+ #
+ # Notify before creating root_factory in case we want to do something
+ # special on a route we may have matched. See
+ # https://github.com/Pylons/pyramid/pull/1876 for ideas of what is
+ # possible.
+ has_listeners and notify(BeforeTraversal(request))
+
+ # Create the root factory
root = root_factory(request)
attrs['root'] = root
- # find a context
+ # We are about to traverse and find a context
traverser = adapters.queryAdapter(root, ITraverser)
if traverser is None:
traverser = ResourceTreeTraverser(root)
@@ -133,6 +143,9 @@ class Router(object):
)
attrs.update(tdict)
+
+ # Notify anyone listening that we have a context and traversal is
+ # complete
has_listeners and notify(ContextFound(request))
# find a view callable
diff --git a/pyramid/scaffolds/alchemy/+package+/__init__.py b/pyramid/scaffolds/alchemy/+package+/__init__.py
index 867049e4f..4dab44823 100644
--- a/pyramid/scaffolds/alchemy/+package+/__init__.py
+++ b/pyramid/scaffolds/alchemy/+package+/__init__.py
@@ -1,21 +1,12 @@
from pyramid.config import Configurator
-from sqlalchemy import engine_from_config
-
-from .models import (
- DBSession,
- Base,
- )
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
config = Configurator(settings=settings)
- config.include('pyramid_chameleon')
- config.add_static_view('static', 'static', cache_max_age=3600)
- config.add_route('home', '/')
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
config.scan()
return config.make_wsgi_app()
diff --git a/pyramid/scaffolds/alchemy/+package+/models.py b/pyramid/scaffolds/alchemy/+package+/models.py
deleted file mode 100644
index a0d3e7b71..000000000
--- a/pyramid/scaffolds/alchemy/+package+/models.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from sqlalchemy import (
- Column,
- Index,
- 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 MyModel(Base):
- __tablename__ = 'models'
- id = Column(Integer, primary_key=True)
- name = Column(Text)
- value = Column(Integer)
-
-Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl
new file mode 100644
index 000000000..26b50aaf6
--- /dev/null
+++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl
@@ -0,0 +1,73 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .mymodel import MyModel # flake8: noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('{{project}}.models')``.
+
+ """
+ settings = config.get_settings()
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/pyramid/scaffolds/alchemy/+package+/models/meta.py b/pyramid/scaffolds/alchemy/+package+/models/meta.py
new file mode 100644
index 000000000..fc3e8f1dd
--- /dev/null
+++ b/pyramid/scaffolds/alchemy/+package+/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/pyramid/scaffolds/alchemy/+package+/models/mymodel.py b/pyramid/scaffolds/alchemy/+package+/models/mymodel.py
new file mode 100644
index 000000000..d65a01a42
--- /dev/null
+++ b/pyramid/scaffolds/alchemy/+package+/models/mymodel.py
@@ -0,0 +1,18 @@
+from sqlalchemy import (
+ Column,
+ Index,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class MyModel(Base):
+ __tablename__ = 'models'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text)
+ value = Column(Integer)
+
+
+Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/pyramid/scaffolds/alchemy/+package+/routes.py b/pyramid/scaffolds/alchemy/+package+/routes.py
new file mode 100644
index 000000000..25504ad4d
--- /dev/null
+++ b/pyramid/scaffolds/alchemy/+package+/routes.py
@@ -0,0 +1,3 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py
index 7dfdece15..7307ecc5c 100644
--- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py
+++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py
@@ -2,8 +2,6 @@ import os
import sys
import transaction
-from sqlalchemy import engine_from_config
-
from pyramid.paster import (
get_appsettings,
setup_logging,
@@ -11,11 +9,13 @@ from pyramid.paster import (
from pyramid.scripts.common import parse_vars
+from ..models.meta import Base
from ..models import (
- DBSession,
- MyModel,
- Base,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
+from ..models import MyModel
def usage(argv):
@@ -32,9 +32,14 @@ def main(argv=sys.argv):
options = parse_vars(argv[2:])
setup_logging(config_uri)
settings = get_appsettings(config_uri, options=options)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
+
+ engine = get_engine(settings)
Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
model = MyModel(name='one', value=1)
- DBSession.add(model)
+ dbsession.add(model)
diff --git a/pyramid/scaffolds/alchemy/+package+/static/theme.min.css b/pyramid/scaffolds/alchemy/+package+/static/theme.min.css
deleted file mode 100644
index 0d25de5b6..000000000
--- a/pyramid/scaffolds/alchemy/+package+/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}}
diff --git a/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl
new file mode 100644
index 000000000..1917f83c7
--- /dev/null
+++ b/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl
index 3f1d23d47..51e382654 100644
--- a/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl
+++ b/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl
@@ -1,12 +1,12 @@
<!DOCTYPE html>
-<html lang="${request.locale_name}">
+<html lang="\{\{request.locale_name\}\}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
- <link rel="shortcut icon" href="${request.static_url('{{package}}:static/pyramid-16x16.png')}">
+ <link rel="shortcut icon" href="\{\{request.static_url('{{package}}:static/pyramid-16x16.png')\}\}">
<title>Alchemy Scaffold for The Pyramid Web Framework</title>
@@ -14,7 +14,7 @@
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
- <link href="${request.static_url('{{package}}:static/theme.css')}" rel="stylesheet">
+ <link href="\{\{request.static_url('{{package}}:static/theme.css')\}\}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -29,13 +29,12 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="${request.static_url('{{package}}:static/pyramid.png')}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="\{\{request.static_url('{{package}}:static/pyramid.png')\}\}" alt="pyramid web framework">
</div>
<div class="col-md-10">
- <div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
- <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework {{pyramid_version}}</span>.</p>
- </div>
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
</div>
</div>
<div class="row">
diff --git a/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl
new file mode 100644
index 000000000..01fe5b8e3
--- /dev/null
+++ b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">\{\{project\}\}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework {{pyramid_version}}</span>.</p>
+</div>
+{% endblock content %}
diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl
index e6425eb91..072eab5b2 100644
--- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl
+++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl
@@ -3,53 +3,63 @@ import transaction
from pyramid import testing
-from .models import DBSession
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
-class TestMyViewSuccessCondition(unittest.TestCase):
+
+class BaseTest(unittest.TestCase):
def setUp(self):
- self.config = testing.setUp()
- from sqlalchemy import create_engine
- engine = create_engine('sqlite://')
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
from .models import (
- Base,
- MyModel,
+ get_engine,
+ get_session_factory,
+ get_tm_session,
)
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = MyModel(name='one', value=55)
- DBSession.add(model)
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
def tearDown(self):
- DBSession.remove()
+ from .models.meta import Base
+
testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
+
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
def test_passing_view(self):
- from .views import my_view
- request = testing.DummyRequest()
- info = my_view(request)
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
self.assertEqual(info['one'].name, 'one')
self.assertEqual(info['project'], '{{project}}')
-class TestMyViewFailureCondition(unittest.TestCase):
- def setUp(self):
- self.config = testing.setUp()
- from sqlalchemy import create_engine
- engine = create_engine('sqlite://')
- from .models import (
- Base,
- MyModel,
- )
- DBSession.configure(bind=engine)
-
- def tearDown(self):
- DBSession.remove()
- testing.tearDown()
+class TestMyViewFailureCondition(BaseTest):
def test_failing_view(self):
- from .views import my_view
- request = testing.DummyRequest()
- info = my_view(request)
- self.assertEqual(info.status_int, 500) \ No newline at end of file
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/pyramid/scaffolds/alchemy/+package+/views/__init__.py b/pyramid/scaffolds/alchemy/+package+/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pyramid/scaffolds/alchemy/+package+/views/__init__.py
diff --git a/pyramid/scaffolds/alchemy/+package+/views.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl
index 292bce579..7bf0026e5 100644
--- a/pyramid/scaffolds/alchemy/+package+/views.py_tmpl
+++ b/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl
@@ -3,22 +3,20 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from .models import (
- DBSession,
- MyModel,
- )
+from ..models import MyModel
-@view_config(route_name='home', renderer='templates/mytemplate.pt')
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(MyModel)
+ one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
- return Response(conn_err_msg, content_type='text/plain', status_int=500)
+ return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': '{{project}}'}
-conn_err_msg = """\
+db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
@@ -33,4 +31,3 @@ might be caused by one of the following things:
After you fix the problem, please restart the Pyramid application to
try it again.
"""
-
diff --git a/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl b/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl
index 0ff6eb7a0..f93f45544 100644
--- a/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl
+++ b/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
-recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/pyramid/scaffolds/alchemy/README.txt_tmpl b/pyramid/scaffolds/alchemy/README.txt_tmpl
index a05c0e174..83c37edea 100644
--- a/pyramid/scaffolds/alchemy/README.txt_tmpl
+++ b/pyramid/scaffolds/alchemy/README.txt_tmpl
@@ -6,7 +6,7 @@ Getting Started
- cd <directory containing this file>
-- $VENV/bin/python setup.py develop
+- $VENV/bin/pip install -e .
- $VENV/bin/initialize_{{project}}_db development.ini
diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl
index 022bc0b7b..4d9f835d4 100644
--- a/pyramid/scaffolds/alchemy/production.ini_tmpl
+++ b/pyramid/scaffolds/alchemy/production.ini_tmpl
@@ -11,8 +11,6 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
-pyramid.includes =
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite
diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl
index 9496b9948..9318817dc 100644
--- a/pyramid/scaffolds/alchemy/setup.py_tmpl
+++ b/pyramid/scaffolds/alchemy/setup.py_tmpl
@@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
requires = [
'pyramid',
- 'pyramid_chameleon',
+ 'pyramid_jinja2',
'pyramid_debugtoolbar',
'pyramid_tm',
'SQLAlchemy',
@@ -19,16 +19,22 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='{{project}}',
version='0.0',
description='{{project}}',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -36,7 +42,9 @@ setup(name='{{project}}',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- test_suite='{{package}}',
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
entry_points="""\
[paste.app_factory]
diff --git a/pyramid/scaffolds/starter/+package+/static/theme.min.css b/pyramid/scaffolds/starter/+package+/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/pyramid/scaffolds/starter/+package+/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/pyramid/scaffolds/starter/+package+/tests.py_tmpl b/pyramid/scaffolds/starter/+package+/tests.py_tmpl
index 94912a850..30f3f0430 100644
--- a/pyramid/scaffolds/starter/+package+/tests.py_tmpl
+++ b/pyramid/scaffolds/starter/+package+/tests.py_tmpl
@@ -15,3 +15,15 @@ class ViewTests(unittest.TestCase):
request = testing.DummyRequest()
info = my_view(request)
self.assertEqual(info['project'], '{{project}}')
+
+
+class FunctionalTests(unittest.TestCase):
+ def setUp(self):
+ from {{package}} import main
+ app = main({})
+ from webtest import TestApp
+ self.testapp = TestApp(app)
+
+ def test_root(self):
+ res = self.testapp.get('/', status=200)
+ self.assertTrue(b'Pyramid' in res.body)
diff --git a/pyramid/scaffolds/starter/README.txt_tmpl b/pyramid/scaffolds/starter/README.txt_tmpl
index 40f98d14a..127ad7595 100644
--- a/pyramid/scaffolds/starter/README.txt_tmpl
+++ b/pyramid/scaffolds/starter/README.txt_tmpl
@@ -1 +1,12 @@
{{project}} README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
+
diff --git a/pyramid/scaffolds/starter/setup.py_tmpl b/pyramid/scaffolds/starter/setup.py_tmpl
index 3802c3e23..2e5ce92c7 100644
--- a/pyramid/scaffolds/starter/setup.py_tmpl
+++ b/pyramid/scaffolds/starter/setup.py_tmpl
@@ -15,16 +15,22 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='{{project}}',
version='0.0',
description='{{project}}',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -32,9 +38,10 @@ setup(name='{{project}}',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
- tests_require=requires,
- test_suite="{{package}}",
entry_points="""\
[paste.app_factory]
main = {{package}}:main
diff --git a/pyramid/scaffolds/zodb/+package+/static/theme.css b/pyramid/scaffolds/zodb/+package+/static/theme.css
index be50ad420..0f4b1a4d4 100644
--- a/pyramid/scaffolds/zodb/+package+/static/theme.css
+++ b/pyramid/scaffolds/zodb/+package+/static/theme.css
@@ -72,10 +72,12 @@ p {
color: #f2b7bd;
font-weight: 400;
}
-.starter-template .links ul li a {
- color: #ffffff;
+.starter-template .links ul li a, a {
+ color: #f2b7bd;
+ text-decoration: underline;
}
-.starter-template .links ul li a:hover {
+.starter-template .links ul li a:hover, a:hover {
+ color: #ffffff;
text-decoration: underline;
}
.starter-template .links ul li .icon-muted {
diff --git a/pyramid/scaffolds/zodb/+package+/static/theme.min.css b/pyramid/scaffolds/zodb/+package+/static/theme.min.css
deleted file mode 100644
index 2f924bcc5..000000000
--- a/pyramid/scaffolds/zodb/+package+/static/theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file
diff --git a/pyramid/scaffolds/zodb/README.txt_tmpl b/pyramid/scaffolds/zodb/README.txt_tmpl
index 40f98d14a..127ad7595 100644
--- a/pyramid/scaffolds/zodb/README.txt_tmpl
+++ b/pyramid/scaffolds/zodb/README.txt_tmpl
@@ -1 +1,12 @@
{{project}} README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
+
diff --git a/pyramid/scaffolds/zodb/setup.py_tmpl b/pyramid/scaffolds/zodb/setup.py_tmpl
index 3a6032429..19771d756 100644
--- a/pyramid/scaffolds/zodb/setup.py_tmpl
+++ b/pyramid/scaffolds/zodb/setup.py_tmpl
@@ -19,16 +19,22 @@ requires = [
'waitress',
]
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
setup(name='{{project}}',
version='0.0',
description='{{project}}',
long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Programming Language :: Python",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
- ],
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
author='',
author_email='',
url='',
@@ -36,9 +42,10 @@ setup(name='{{project}}',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
install_requires=requires,
- tests_require=requires,
- test_suite="{{package}}",
entry_points="""\
[paste.app_factory]
main = {{package}}:main
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index 431afe6f4..74bda1dce 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -362,7 +362,7 @@ a real process manager for your processes like Systemd, Circus, or Supervisor.
log_fn = None
if log_fn:
log_fn = os.path.join(base, log_fn)
- setup_logging(log_fn)
+ setup_logging(log_fn, global_conf=vars)
server = self.loadserver(server_spec, name=server_name,
relative_to=base, global_conf=vars)
diff --git a/pyramid/session.py b/pyramid/session.py
index a4cdf910d..36ebc2f00 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -16,11 +16,19 @@ from pyramid.compat import (
text_,
bytes_,
native_,
+ urlparse,
)
-from pyramid.exceptions import BadCSRFToken
+from pyramid.exceptions import (
+ BadCSRFOrigin,
+ BadCSRFToken,
+)
from pyramid.interfaces import ISession
-from pyramid.util import strings_differ
+from pyramid.settings import aslist
+from pyramid.util import (
+ is_same_domain,
+ strings_differ,
+)
def manage_accessed(wrapped):
""" Decorator which causes a cookie to be renewed when an accessor
@@ -101,18 +109,110 @@ def signed_deserialize(serialized, secret, hmac=hmac):
return pickle.loads(pickled)
+
+def check_csrf_origin(request, trusted_origins=None, raises=True):
+ """
+ Check the Origin of the request to see if it is a cross site request or
+ not.
+
+ If the value supplied by the Origin or Referer header isn't one of the
+ trusted origins and ``raises`` is ``True``, this function will raise a
+ :exc:`pyramid.exceptions.BadCSRFOrigin` exception but if ``raises`` is
+ ``False`` this function will return ``False`` instead. If the CSRF origin
+ checks are successful this function will return ``True`` unconditionally.
+
+ Additional trusted origins may be added by passing a list of domain (and
+ ports if nonstandard like `['example.com', 'dev.example.com:8080']`) in
+ with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None``
+ (the default) this list of additional domains will be pulled from the
+ ``pyramid.csrf_trusted_origins`` setting.
+
+ Note that this function will do nothing if request.scheme is not https.
+
+ .. versionadded:: 1.7
+ """
+ def _fail(reason):
+ if raises:
+ raise BadCSRFOrigin(reason)
+ else:
+ return False
+
+ if request.scheme == "https":
+ # Suppose user visits http://example.com/
+ # An active network attacker (man-in-the-middle, MITM) sends a
+ # POST form that targets https://example.com/detonate-bomb/ and
+ # submits it via JavaScript.
+ #
+ # The attacker will need to provide a CSRF cookie and token, but
+ # that's no problem for a MITM when we cannot make any assumptions
+ # about what kind of session storage is being used. So the MITM can
+ # circumvent the CSRF protection. This is true for any HTTP connection,
+ # but anyone using HTTPS expects better! For this reason, for
+ # https://example.com/ we need additional protection that treats
+ # http://example.com/ as completely untrusted. Under HTTPS,
+ # Barth et al. found that the Referer header is missing for
+ # same-domain requests in only about 0.2% of cases or less, so
+ # we can use strict Referer checking.
+
+ # Determine the origin of this request
+ origin = request.headers.get("Origin")
+ if origin is None:
+ origin = request.referrer
+
+ # Fail if we were not able to locate an origin at all
+ if not origin:
+ return _fail("Origin checking failed - no Origin or Referer.")
+
+ # Parse our origin so we we can extract the required information from
+ # it.
+ originp = urlparse.urlparse(origin)
+
+ # Ensure that our Referer is also secure.
+ if originp.scheme != "https":
+ return _fail(
+ "Referer checking failed - Referer is insecure while host is "
+ "secure."
+ )
+
+ # Determine which origins we trust, which by default will include the
+ # current origin.
+ if trusted_origins is None:
+ trusted_origins = aslist(
+ request.registry.settings.get(
+ "pyramid.csrf_trusted_origins", [])
+ )
+
+ if request.host_port not in set([80, 443]):
+ trusted_origins.append("{0.domain}:{0.host_port}".format(request))
+ else:
+ trusted_origins.append(request.domain)
+
+ # Actually check to see if the request's origin matches any of our
+ # trusted origins.
+ if not any(is_same_domain(originp.netloc, host)
+ for host in trusted_origins):
+ reason = (
+ "Referer checking failed - {0} does not match any trusted "
+ "origins."
+ )
+ return _fail(reason.format(origin))
+
+ return True
+
+
def check_csrf_token(request,
token='csrf_token',
header='X-CSRF-Token',
raises=True):
""" Check the CSRF token in the request's session against the value in
- ``request.params.get(token)`` or ``request.headers.get(header)``.
- If a ``token`` keyword is not supplied to this function, the string
- ``csrf_token`` will be used to look up the token in ``request.params``.
- If a ``header`` keyword is not supplied to this function, the string
- ``X-CSRF-Token`` will be used to look up the token in ``request.headers``.
-
- If the value supplied by param or by header doesn't match the value
+ ``request.POST.get(token)`` (if a POST request) or
+ ``request.headers.get(header)``. If a ``token`` keyword is not supplied to
+ this function, the string ``csrf_token`` will be used to look up the token
+ in ``request.POST``. If a ``header`` keyword is not supplied to this
+ function, the string ``X-CSRF-Token`` will be used to look up the token in
+ ``request.headers``.
+
+ If the value supplied by post or by header doesn't match the value
supplied by ``request.session.get_csrf_token()``, and ``raises`` is
``True``, this function will raise an
:exc:`pyramid.exceptions.BadCSRFToken` exception.
@@ -123,9 +223,28 @@ def check_csrf_token(request,
Note that using this function requires that a :term:`session factory` is
configured.
+ See :ref:`auto_csrf_checking` for information about how to secure your
+ application automatically against CSRF attacks.
+
.. versionadded:: 1.4a2
+
+ .. versionchanged:: 1.7a1
+ A CSRF token passed in the query string of the request is no longer
+ considered valid. It must be passed in either the request body or
+ a header.
"""
- supplied_token = request.params.get(token, request.headers.get(header, ""))
+ # If this is a POST/PUT/etc request, then we'll check the body to see if it
+ # has a token. We explicitly use request.POST here because CSRF tokens
+ # should never appear in an URL as doing so is a security issue. We also
+ # explicitly check for request.POST here as we do not support sending form
+ # encoded data over anything but a request.POST.
+ supplied_token = request.POST.get(token, "")
+
+ # If we were unable to locate a CSRF token in a request body, then we'll
+ # check to see if there are any headers that have a value for us.
+ if supplied_token == "":
+ supplied_token = request.headers.get(header, "")
+
expected_token = request.session.get_csrf_token()
if strings_differ(bytes_(expected_token), bytes_(supplied_token)):
if raises:
diff --git a/pyramid/settings.py b/pyramid/settings.py
index e2cb3cb3c..8a498d572 100644
--- a/pyramid/settings.py
+++ b/pyramid/settings.py
@@ -1,13 +1,12 @@
from pyramid.compat import string_types
truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
+falsey = frozenset(('f', 'false', 'n', 'no', 'off', '0'))
def asbool(s):
""" Return the boolean value ``True`` if the case-lowered value of string
- input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise
- return the boolean value ``False``. If ``s`` is the value ``None``,
- return ``False``. If ``s`` is already one of the boolean values ``True``
- or ``False``, return it."""
+ input ``s`` is a :term:`truthy string`. If ``s`` is already one of the
+ boolean values ``True`` or ``False``, return it."""
if s is None:
return False
if isinstance(s, bool):
diff --git a/pyramid/static.py b/pyramid/static.py
index 4054d5be0..0965be95c 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -229,9 +229,9 @@ class ManifestCacheBuster(object):
By default, it is a JSON-serialized dictionary where the keys are the
source asset paths used in calls to
- :meth:`~pyramid.request.Request.static_url`. For example::
+ :meth:`~pyramid.request.Request.static_url`. For example:
- .. code-block:: python
+ .. code-block:: pycon
>>> request.static_url('myapp:static/css/main.css')
"http://www.example.com/static/css/main-678b7c80.css"
diff --git a/pyramid/testing.py b/pyramid/testing.py
index 14432b01f..ec06fe379 100644
--- a/pyramid/testing.py
+++ b/pyramid/testing.py
@@ -41,6 +41,7 @@ from pyramid.i18n import LocalizerRequestMixin
from pyramid.request import CallbackMethodsMixin
from pyramid.url import URLMethodsMixin
from pyramid.util import InstancePropertyMixin
+from pyramid.view import ViewMethodsMixin
_marker = object()
@@ -293,6 +294,7 @@ class DummyRequest(
LocalizerRequestMixin,
AuthenticationAPIMixin,
AuthorizationAPIMixin,
+ ViewMethodsMixin,
):
""" A DummyRequest object (incompletely) imitates a :term:`request` object.
@@ -474,6 +476,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True,
# method.
config.add_default_renderers()
config.add_default_view_predicates()
+ config.add_default_view_derivers()
config.add_default_route_predicates()
config.commit()
global have_zca
diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py
index 1cd6050bf..9cd8f2734 100644
--- a/pyramid/tests/test_config/test_predicates.py
+++ b/pyramid/tests/test_config/test_predicates.py
@@ -120,9 +120,9 @@ class TestRequestParamPredicate(unittest.TestCase):
self.assertTrue(result)
def test___call___true_multi(self):
- inst = self._makeOne(('abc', 'def =2 '))
+ inst = self._makeOne(('abc', '=def =2= '))
request = Dummy()
- request.params = {'abc':'1', 'def': '2'}
+ request.params = {'abc':'1', '=def': '2='}
result = inst(None, request)
self.assertTrue(result)
@@ -144,6 +144,10 @@ class TestRequestParamPredicate(unittest.TestCase):
inst = self._makeOne('abc')
self.assertEqual(inst.text(), 'request_param abc')
+ def test_text_exists_equal_sign(self):
+ inst = self._makeOne('=abc')
+ self.assertEqual(inst.text(), 'request_param =abc')
+
def test_text_withval(self):
inst = self._makeOne('abc= 1')
self.assertEqual(inst.text(), 'request_param abc=1')
@@ -152,10 +156,18 @@ class TestRequestParamPredicate(unittest.TestCase):
inst = self._makeOne(('abc= 1', 'def'))
self.assertEqual(inst.text(), 'request_param abc=1,def')
+ def test_text_multi_equal_sign(self):
+ inst = self._makeOne(('abc= 1', '=def= 2'))
+ self.assertEqual(inst.text(), 'request_param =def=2,abc=1')
+
def test_phash_exists(self):
inst = self._makeOne('abc')
self.assertEqual(inst.phash(), 'request_param abc')
+ def test_phash_exists_equal_sign(self):
+ inst = self._makeOne('=abc')
+ self.assertEqual(inst.phash(), 'request_param =abc')
+
def test_phash_withval(self):
inst = self._makeOne('abc= 1')
self.assertEqual(inst.phash(), "request_param abc=1")
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index e89d43c9a..21ed24f44 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1491,6 +1491,23 @@ class TestViewsConfigurationMixin(unittest.TestCase):
request.upath_info = text_('/')
self._assertNotFound(wrapper, None, request)
+ def test_add_view_with_check_csrf_predicates_match(self):
+ import warnings
+ from pyramid.renderers import null_renderer
+ view = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ with warnings.catch_warnings(record=True) as w:
+ warnings.filterwarnings('always')
+ config.add_view(view=view, check_csrf=True, renderer=null_renderer)
+ self.assertEqual(len(w), 1)
+ wrapper = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.method = "POST"
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'csrf_token': 'foo'}
+ request.headers = {}
+ self.assertEqual(wrapper(None, request), 'OK')
+
def test_add_view_with_custom_predicates_match(self):
import warnings
from pyramid.renderers import null_renderer
@@ -1570,6 +1587,51 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=view2)
self.assertRaises(ConfigurationConflictError, config.commit)
+ def test_add_view_with_csrf_param(self):
+ from pyramid.renderers import null_renderer
+ def view(request):
+ return 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view, require_csrf='st', renderer=null_renderer)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.scheme = "http"
+ request.method = 'POST'
+ request.POST = {'st': 'foo'}
+ request.headers = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.assertEqual(view(None, request), 'OK')
+
+ def test_add_view_with_csrf_header(self):
+ from pyramid.renderers import null_renderer
+ def view(request):
+ return 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view, require_csrf=True, renderer=null_renderer)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.scheme = "http"
+ request.method = 'POST'
+ request.POST = {}
+ request.headers = {'X-CSRF-Token': 'foo'}
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.assertEqual(view(None, request), 'OK')
+
+ def test_add_view_with_missing_csrf_header(self):
+ from pyramid.exceptions import BadCSRFToken
+ from pyramid.renderers import null_renderer
+ def view(request): return 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view, require_csrf=True, renderer=null_renderer)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ request.scheme = "http"
+ request.method = 'POST'
+ request.POST = {}
+ request.headers = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
def test_add_view_with_permission(self):
from pyramid.renderers import null_renderer
view1 = lambda *arg: 'OK'
@@ -2467,1135 +2529,7 @@ class TestMultiView(unittest.TestCase):
response = mv(context, request)
self.assertEqual(response, expected_response)
-class TestViewDeriver(unittest.TestCase):
- def setUp(self):
- self.config = testing.setUp()
-
- def tearDown(self):
- self.config = None
-
- def _makeOne(self, **kw):
- kw['registry'] = self.config.registry
- from pyramid.config.views import ViewDeriver
- return ViewDeriver(**kw)
-
- def _makeRequest(self):
- request = DummyRequest()
- request.registry = self.config.registry
- return request
-
- def _registerLogger(self):
- from pyramid.interfaces import IDebugLogger
- logger = DummyLogger()
- self.config.registry.registerUtility(logger, IDebugLogger)
- return logger
-
- def _registerSecurityPolicy(self, permissive):
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
- policy = DummySecurityPolicy(permissive)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
-
- def test_function_returns_non_adaptable(self):
- def view(request):
- return None
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- try:
- result(None, None)
- except ValueError as e:
- self.assertEqual(
- e.args[0],
- 'Could not convert return value of the view callable function '
- 'pyramid.tests.test_config.test_views.view into a response '
- 'object. The value returned was None. You may have forgotten '
- 'to return a value from the view callable.'
- )
- else: # pragma: no cover
- raise AssertionError
-
- def test_function_returns_non_adaptable_dict(self):
- def view(request):
- return {'a':1}
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- try:
- result(None, None)
- except ValueError as e:
- self.assertEqual(
- e.args[0],
- "Could not convert return value of the view callable function "
- "pyramid.tests.test_config.test_views.view into a response "
- "object. The value returned was {'a': 1}. You may have "
- "forgotten to define a renderer in the view configuration."
- )
- else: # pragma: no cover
- raise AssertionError
-
- def test_instance_returns_non_adaptable(self):
- class AView(object):
- def __call__(self, request):
- return None
- view = AView()
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- try:
- result(None, None)
- except ValueError as e:
- msg = e.args[0]
- self.assertTrue(msg.startswith(
- 'Could not convert return value of the view callable object '
- '<pyramid.tests.test_config.test_views.'))
- self.assertTrue(msg.endswith(
- '> into a response object. The value returned was None. You '
- 'may have forgotten to return a value from the view callable.'))
- else: # pragma: no cover
- raise AssertionError
-
- def test_function_returns_true_Response_no_renderer(self):
- from pyramid.response import Response
- r = Response('Hello')
- def view(request):
- return r
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- response = result(None, None)
- self.assertEqual(response, r)
-
- def test_function_returns_true_Response_with_renderer(self):
- from pyramid.response import Response
- r = Response('Hello')
- def view(request):
- return r
- renderer = object()
- deriver = self._makeOne(renderer=renderer)
- result = deriver(view)
- self.assertFalse(result is view)
- response = result(None, None)
- self.assertEqual(response, r)
-
- def test_requestonly_default_method_returns_non_adaptable(self):
- request = DummyRequest()
- class AView(object):
- def __init__(self, request):
- pass
- def __call__(self):
- return None
- deriver = self._makeOne()
- result = deriver(AView)
- self.assertFalse(result is AView)
- try:
- result(None, request)
- except ValueError as e:
- self.assertEqual(
- e.args[0],
- 'Could not convert return value of the view callable '
- 'method __call__ of '
- 'class pyramid.tests.test_config.test_views.AView into a '
- 'response object. The value returned was None. You may have '
- 'forgotten to return a value from the view callable.'
- )
- else: # pragma: no cover
- raise AssertionError
-
- def test_requestonly_nondefault_method_returns_non_adaptable(self):
- request = DummyRequest()
- class AView(object):
- def __init__(self, request):
- pass
- def theviewmethod(self):
- return None
- deriver = self._makeOne(attr='theviewmethod')
- result = deriver(AView)
- self.assertFalse(result is AView)
- try:
- result(None, request)
- except ValueError as e:
- self.assertEqual(
- e.args[0],
- 'Could not convert return value of the view callable '
- 'method theviewmethod of '
- 'class pyramid.tests.test_config.test_views.AView into a '
- 'response object. The value returned was None. You may have '
- 'forgotten to return a value from the view callable.'
- )
- else: # pragma: no cover
- raise AssertionError
-
- def test_requestonly_function(self):
- response = DummyResponse()
- def view(request):
- return response
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(result(None, None), response)
-
- def test_requestonly_function_with_renderer(self):
- response = DummyResponse()
- class moo(object):
- def render_view(inself, req, resp, view_inst, ctx):
- self.assertEqual(req, request)
- self.assertEqual(resp, 'OK')
- self.assertEqual(view_inst, view)
- self.assertEqual(ctx, context)
- return response
- def clone(self):
- return self
- def view(request):
- return 'OK'
- deriver = self._makeOne(renderer=moo())
- result = deriver(view)
- self.assertFalse(result.__wraps__ is view)
- request = self._makeRequest()
- context = testing.DummyResource()
- self.assertEqual(result(context, request), response)
-
- def test_requestonly_function_with_renderer_request_override(self):
- def moo(info):
- def inner(value, system):
- self.assertEqual(value, 'OK')
- self.assertEqual(system['request'], request)
- self.assertEqual(system['context'], context)
- return b'moo'
- return inner
- def view(request):
- return 'OK'
- self.config.add_renderer('moo', moo)
- deriver = self._makeOne(renderer='string')
- result = deriver(view)
- self.assertFalse(result is view)
- request = self._makeRequest()
- request.override_renderer = 'moo'
- context = testing.DummyResource()
- self.assertEqual(result(context, request).body, b'moo')
-
- def test_requestonly_function_with_renderer_request_has_view(self):
- response = DummyResponse()
- class moo(object):
- def render_view(inself, req, resp, view_inst, ctx):
- self.assertEqual(req, request)
- self.assertEqual(resp, 'OK')
- self.assertEqual(view_inst, 'view')
- self.assertEqual(ctx, context)
- return response
- def clone(self):
- return self
- def view(request):
- return 'OK'
- deriver = self._makeOne(renderer=moo())
- result = deriver(view)
- self.assertFalse(result.__wraps__ is view)
- request = self._makeRequest()
- request.__view__ = 'view'
- context = testing.DummyResource()
- r = result(context, request)
- self.assertEqual(r, response)
- self.assertFalse(hasattr(request, '__view__'))
-
- def test_class_without_attr(self):
- response = DummyResponse()
- class View(object):
- def __init__(self, request):
- pass
- def __call__(self):
- return response
- deriver = self._makeOne()
- result = deriver(View)
- request = self._makeRequest()
- self.assertEqual(result(None, request), response)
- self.assertEqual(request.__view__.__class__, View)
-
- def test_class_with_attr(self):
- response = DummyResponse()
- class View(object):
- def __init__(self, request):
- pass
- def another(self):
- return response
- deriver = self._makeOne(attr='another')
- result = deriver(View)
- request = self._makeRequest()
- self.assertEqual(result(None, request), response)
- self.assertEqual(request.__view__.__class__, View)
-
- def test_as_function_context_and_request(self):
- def view(context, request):
- return 'OK'
- deriver = self._makeOne()
- result = deriver(view)
- self.assertTrue(result.__wraps__ is view)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- self.assertEqual(view(None, None), 'OK')
-
- def test_as_function_requestonly(self):
- response = DummyResponse()
- def view(request):
- return response
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- self.assertEqual(result(None, None), response)
-
- def test_as_newstyle_class_context_and_request(self):
- response = DummyResponse()
- class view(object):
- def __init__(self, context, request):
- pass
- def __call__(self):
- return response
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- self.assertEqual(result(None, request), response)
- self.assertEqual(request.__view__.__class__, view)
-
- def test_as_newstyle_class_requestonly(self):
- response = DummyResponse()
- class view(object):
- def __init__(self, context, request):
- pass
- def __call__(self):
- return response
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- self.assertEqual(result(None, request), response)
- self.assertEqual(request.__view__.__class__, view)
-
- def test_as_oldstyle_class_context_and_request(self):
- response = DummyResponse()
- class view:
- def __init__(self, context, request):
- pass
- def __call__(self):
- return response
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- self.assertEqual(result(None, request), response)
- self.assertEqual(request.__view__.__class__, view)
-
- def test_as_oldstyle_class_requestonly(self):
- response = DummyResponse()
- class view:
- def __init__(self, context, request):
- pass
- def __call__(self):
- return response
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- self.assertEqual(result(None, request), response)
- self.assertEqual(request.__view__.__class__, view)
-
- def test_as_instance_context_and_request(self):
- response = DummyResponse()
- class View:
- def __call__(self, context, request):
- return response
- view = View()
- deriver = self._makeOne()
- result = deriver(view)
- self.assertTrue(result.__wraps__ is view)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- self.assertEqual(result(None, None), response)
-
- def test_as_instance_requestonly(self):
- response = DummyResponse()
- class View:
- def __call__(self, request):
- return response
- view = View()
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertTrue('test_views' in result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- self.assertEqual(result(None, None), response)
-
- def test_with_debug_authorization_no_authpol(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(
- debug_authorization=True, reload_templates=True)
- logger = self._registerLogger()
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): Allowed "
- "(no authorization policy in use)")
-
- def test_with_debug_authorization_authn_policy_no_authz_policy(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(debug_authorization=True)
- from pyramid.interfaces import IAuthenticationPolicy
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- logger = self._registerLogger()
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): Allowed "
- "(no authorization policy in use)")
-
- def test_with_debug_authorization_authz_policy_no_authn_policy(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(debug_authorization=True)
- from pyramid.interfaces import IAuthorizationPolicy
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
- logger = self._registerLogger()
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): Allowed "
- "(no authorization policy in use)")
-
- def test_with_debug_authorization_no_permission(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(
- debug_authorization=True, reload_templates=True)
- self._registerSecurityPolicy(True)
- logger = self._registerLogger()
- deriver = self._makeOne()
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): Allowed ("
- "no permission registered)")
-
- def test_debug_auth_permission_authpol_permitted(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(
- debug_authorization=True, reload_templates=True)
- logger = self._registerLogger()
- self._registerSecurityPolicy(True)
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertEqual(result.__call_permissive__.__wraps__, view)
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): True")
-
- def test_debug_auth_permission_authpol_permitted_no_request(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(
- debug_authorization=True, reload_templates=True)
- logger = self._registerLogger()
- self._registerSecurityPolicy(True)
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertEqual(result.__call_permissive__.__wraps__, view)
- self.assertEqual(result(None, None), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(logger.messages[0],
- "debug_authorization of url None (view name "
- "None against context None): True")
-
- def test_debug_auth_permission_authpol_denied(self):
- from pyramid.httpexceptions import HTTPForbidden
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(
- debug_authorization=True, reload_templates=True)
- logger = self._registerLogger()
- self._registerSecurityPolicy(False)
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertEqual(result.__call_permissive__.__wraps__, view)
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertRaises(HTTPForbidden, result, None, request)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): False")
-
- def test_debug_auth_permission_authpol_denied2(self):
- view = lambda *arg: 'OK'
- self.config.registry.settings = dict(
- debug_authorization=True, reload_templates=True)
- self._registerLogger()
- self._registerSecurityPolicy(False)
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- permitted = result.__permitted__(None, None)
- self.assertEqual(permitted, False)
-
- def test_debug_auth_permission_authpol_overridden(self):
- from pyramid.security import NO_PERMISSION_REQUIRED
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = dict(
- debug_authorization=True, reload_templates=True)
- logger = self._registerLogger()
- self._registerSecurityPolicy(False)
- deriver = self._makeOne(permission=NO_PERMISSION_REQUIRED)
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
- self.assertEqual(len(logger.messages), 1)
- self.assertEqual(logger.messages[0],
- "debug_authorization of url url (view name "
- "'view_name' against context None): "
- "Allowed (NO_PERMISSION_REQUIRED)")
-
- def test_secured_view_authn_policy_no_authz_policy(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = {}
- from pyramid.interfaces import IAuthenticationPolicy
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
-
- def test_secured_view_authz_policy_no_authn_policy(self):
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = {}
- from pyramid.interfaces import IAuthorizationPolicy
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- self.assertEqual(view.__module__, result.__module__)
- self.assertEqual(view.__doc__, result.__doc__)
- self.assertEqual(view.__name__, result.__name__)
- self.assertFalse(hasattr(result, '__call_permissive__'))
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- self.assertEqual(result(None, request), response)
-
- def test_secured_view_raises_forbidden_no_name(self):
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
- from pyramid.httpexceptions import HTTPForbidden
- response = DummyResponse()
- view = lambda *arg: response
- self.config.registry.settings = {}
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
- deriver = self._makeOne(permission='view')
- result = deriver(view)
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- try:
- result(None, request)
- except HTTPForbidden as e:
- self.assertEqual(e.message,
- 'Unauthorized: <lambda> failed permission check')
- else: # pragma: no cover
- raise AssertionError
-
- def test_secured_view_raises_forbidden_with_name(self):
- from pyramid.interfaces import IAuthenticationPolicy
- from pyramid.interfaces import IAuthorizationPolicy
- from pyramid.httpexceptions import HTTPForbidden
- def myview(request): pass
- self.config.registry.settings = {}
- policy = DummySecurityPolicy(False)
- self.config.registry.registerUtility(policy, IAuthenticationPolicy)
- self.config.registry.registerUtility(policy, IAuthorizationPolicy)
- deriver = self._makeOne(permission='view')
- result = deriver(myview)
- request = self._makeRequest()
- request.view_name = 'view_name'
- request.url = 'url'
- try:
- result(None, request)
- except HTTPForbidden as e:
- self.assertEqual(e.message,
- 'Unauthorized: myview failed permission check')
- else: # pragma: no cover
- raise AssertionError
-
- def test_predicate_mismatch_view_has_no_name(self):
- from pyramid.exceptions import PredicateMismatch
- response = DummyResponse()
- view = lambda *arg: response
- def predicate1(context, request):
- return False
- predicate1.text = lambda *arg: 'text'
- deriver = self._makeOne(predicates=[predicate1])
- result = deriver(view)
- request = self._makeRequest()
- request.method = 'POST'
- try:
- result(None, None)
- except PredicateMismatch as e:
- self.assertEqual(e.detail,
- 'predicate mismatch for view <lambda> (text)')
- else: # pragma: no cover
- raise AssertionError
-
- def test_predicate_mismatch_view_has_name(self):
- from pyramid.exceptions import PredicateMismatch
- def myview(request): pass
- def predicate1(context, request):
- return False
- predicate1.text = lambda *arg: 'text'
- deriver = self._makeOne(predicates=[predicate1])
- result = deriver(myview)
- request = self._makeRequest()
- request.method = 'POST'
- try:
- result(None, None)
- except PredicateMismatch as e:
- self.assertEqual(e.detail,
- 'predicate mismatch for view myview (text)')
- else: # pragma: no cover
- raise AssertionError
-
- def test_predicate_mismatch_exception_has_text_in_detail(self):
- from pyramid.exceptions import PredicateMismatch
- def myview(request): pass
- def predicate1(context, request):
- return True
- predicate1.text = lambda *arg: 'pred1'
- def predicate2(context, request):
- return False
- predicate2.text = lambda *arg: 'pred2'
- deriver = self._makeOne(predicates=[predicate1, predicate2])
- result = deriver(myview)
- request = self._makeRequest()
- request.method = 'POST'
- try:
- result(None, None)
- except PredicateMismatch as e:
- self.assertEqual(e.detail,
- 'predicate mismatch for view myview (pred2)')
- else: # pragma: no cover
- raise AssertionError
-
- def test_with_predicates_all(self):
- response = DummyResponse()
- view = lambda *arg: response
- predicates = []
- def predicate1(context, request):
- predicates.append(True)
- return True
- def predicate2(context, request):
- predicates.append(True)
- return True
- deriver = self._makeOne(predicates=[predicate1, predicate2])
- result = deriver(view)
- request = self._makeRequest()
- request.method = 'POST'
- next = result(None, None)
- self.assertEqual(next, response)
- self.assertEqual(predicates, [True, True])
-
- def test_with_predicates_checker(self):
- view = lambda *arg: 'OK'
- predicates = []
- def predicate1(context, request):
- predicates.append(True)
- return True
- def predicate2(context, request):
- predicates.append(True)
- return True
- deriver = self._makeOne(predicates=[predicate1, predicate2])
- result = deriver(view)
- request = self._makeRequest()
- request.method = 'POST'
- next = result.__predicated__(None, None)
- self.assertEqual(next, True)
- self.assertEqual(predicates, [True, True])
- def test_with_predicates_notall(self):
- from pyramid.httpexceptions import HTTPNotFound
- view = lambda *arg: 'OK'
- predicates = []
- def predicate1(context, request):
- predicates.append(True)
- return True
- predicate1.text = lambda *arg: 'text'
- def predicate2(context, request):
- predicates.append(True)
- return False
- predicate2.text = lambda *arg: 'text'
- deriver = self._makeOne(predicates=[predicate1, predicate2])
- result = deriver(view)
- request = self._makeRequest()
- request.method = 'POST'
- self.assertRaises(HTTPNotFound, result, None, None)
- self.assertEqual(predicates, [True, True])
-
- def test_with_wrapper_viewname(self):
- from pyramid.response import Response
- from pyramid.interfaces import IView
- from pyramid.interfaces import IViewClassifier
- inner_response = Response('OK')
- def inner_view(context, request):
- return inner_response
- def outer_view(context, request):
- self.assertEqual(request.wrapped_response, inner_response)
- self.assertEqual(request.wrapped_body, inner_response.body)
- self.assertEqual(request.wrapped_view.__original_view__,
- inner_view)
- return Response(b'outer ' + request.wrapped_body)
- self.config.registry.registerAdapter(
- outer_view, (IViewClassifier, None, None), IView, 'owrap')
- deriver = self._makeOne(viewname='inner',
- wrapper_viewname='owrap')
- result = deriver(inner_view)
- self.assertFalse(result is inner_view)
- self.assertEqual(inner_view.__module__, result.__module__)
- self.assertEqual(inner_view.__doc__, result.__doc__)
- request = self._makeRequest()
- response = result(None, request)
- self.assertEqual(response.body, b'outer OK')
-
- def test_with_wrapper_viewname_notfound(self):
- from pyramid.response import Response
- inner_response = Response('OK')
- def inner_view(context, request):
- return inner_response
- deriver = self._makeOne(viewname='inner', wrapper_viewname='owrap')
- wrapped = deriver(inner_view)
- request = self._makeRequest()
- self.assertRaises(ValueError, wrapped, None, request)
-
- def test_as_newstyle_class_context_and_request_attr_and_renderer(self):
- response = DummyResponse()
- class renderer(object):
- def render_view(inself, req, resp, view_inst, ctx):
- self.assertEqual(req, request)
- self.assertEqual(resp, {'a':'1'})
- self.assertEqual(view_inst.__class__, View)
- self.assertEqual(ctx, context)
- return response
- def clone(self):
- return self
- class View(object):
- def __init__(self, context, request):
- pass
- def index(self):
- return {'a':'1'}
- deriver = self._makeOne(renderer=renderer(), attr='index')
- result = deriver(View)
- self.assertFalse(result is View)
- self.assertEqual(result.__module__, View.__module__)
- self.assertEqual(result.__doc__, View.__doc__)
- self.assertEqual(result.__name__, View.__name__)
- request = self._makeRequest()
- context = testing.DummyResource()
- self.assertEqual(result(context, request), response)
-
- def test_as_newstyle_class_requestonly_attr_and_renderer(self):
- response = DummyResponse()
- class renderer(object):
- def render_view(inself, req, resp, view_inst, ctx):
- self.assertEqual(req, request)
- self.assertEqual(resp, {'a':'1'})
- self.assertEqual(view_inst.__class__, View)
- self.assertEqual(ctx, context)
- return response
- def clone(self):
- return self
- class View(object):
- def __init__(self, request):
- pass
- def index(self):
- return {'a':'1'}
- deriver = self._makeOne(renderer=renderer(), attr='index')
- result = deriver(View)
- self.assertFalse(result is View)
- self.assertEqual(result.__module__, View.__module__)
- self.assertEqual(result.__doc__, View.__doc__)
- self.assertEqual(result.__name__, View.__name__)
- request = self._makeRequest()
- context = testing.DummyResource()
- self.assertEqual(result(context, request), response)
-
- def test_as_oldstyle_cls_context_request_attr_and_renderer(self):
- response = DummyResponse()
- class renderer(object):
- def render_view(inself, req, resp, view_inst, ctx):
- self.assertEqual(req, request)
- self.assertEqual(resp, {'a':'1'})
- self.assertEqual(view_inst.__class__, View)
- self.assertEqual(ctx, context)
- return response
- def clone(self):
- return self
- class View:
- def __init__(self, context, request):
- pass
- def index(self):
- return {'a':'1'}
- deriver = self._makeOne(renderer=renderer(), attr='index')
- result = deriver(View)
- self.assertFalse(result is View)
- self.assertEqual(result.__module__, View.__module__)
- self.assertEqual(result.__doc__, View.__doc__)
- self.assertEqual(result.__name__, View.__name__)
- request = self._makeRequest()
- context = testing.DummyResource()
- self.assertEqual(result(context, request), response)
-
- def test_as_oldstyle_cls_requestonly_attr_and_renderer(self):
- response = DummyResponse()
- class renderer(object):
- def render_view(inself, req, resp, view_inst, ctx):
- self.assertEqual(req, request)
- self.assertEqual(resp, {'a':'1'})
- self.assertEqual(view_inst.__class__, View)
- self.assertEqual(ctx, context)
- return response
- def clone(self):
- return self
- class View:
- def __init__(self, request):
- pass
- def index(self):
- return {'a':'1'}
- deriver = self._makeOne(renderer=renderer(), attr='index')
- result = deriver(View)
- self.assertFalse(result is View)
- self.assertEqual(result.__module__, View.__module__)
- self.assertEqual(result.__doc__, View.__doc__)
- self.assertEqual(result.__name__, View.__name__)
- request = self._makeRequest()
- context = testing.DummyResource()
- self.assertEqual(result(context, request), response)
-
- def test_as_instance_context_and_request_attr_and_renderer(self):
- response = DummyResponse()
- class renderer(object):
- def render_view(inself, req, resp, view_inst, ctx):
- self.assertEqual(req, request)
- self.assertEqual(resp, {'a':'1'})
- self.assertEqual(view_inst, view)
- self.assertEqual(ctx, context)
- return response
- def clone(self):
- return self
- class View:
- def index(self, context, request):
- return {'a':'1'}
- deriver = self._makeOne(renderer=renderer(), attr='index')
- view = View()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(result.__module__, view.__module__)
- self.assertEqual(result.__doc__, view.__doc__)
- request = self._makeRequest()
- context = testing.DummyResource()
- self.assertEqual(result(context, request), response)
-
- def test_as_instance_requestonly_attr_and_renderer(self):
- response = DummyResponse()
- class renderer(object):
- def render_view(inself, req, resp, view_inst, ctx):
- self.assertEqual(req, request)
- self.assertEqual(resp, {'a':'1'})
- self.assertEqual(view_inst, view)
- self.assertEqual(ctx, context)
- return response
- def clone(self):
- return self
- class View:
- def index(self, request):
- return {'a':'1'}
- deriver = self._makeOne(renderer=renderer(), attr='index')
- view = View()
- result = deriver(view)
- self.assertFalse(result is view)
- self.assertEqual(result.__module__, view.__module__)
- self.assertEqual(result.__doc__, view.__doc__)
- request = self._makeRequest()
- context = testing.DummyResource()
- self.assertEqual(result(context, request), response)
-
- def test_with_view_mapper_config_specified(self):
- response = DummyResponse()
- class mapper(object):
- def __init__(self, **kw):
- self.kw = kw
- def __call__(self, view):
- def wrapped(context, request):
- return response
- return wrapped
- def view(context, request): return 'NOTOK'
- deriver = self._makeOne(mapper=mapper)
- result = deriver(view)
- self.assertFalse(result.__wraps__ is view)
- self.assertEqual(result(None, None), response)
-
- def test_with_view_mapper_view_specified(self):
- from pyramid.response import Response
- response = Response()
- def mapper(**kw):
- def inner(view):
- def superinner(context, request):
- self.assertEqual(request, None)
- return response
- return superinner
- return inner
- def view(context, request): return 'NOTOK'
- view.__view_mapper__ = mapper
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result.__wraps__ is view)
- self.assertEqual(result(None, None), response)
-
- def test_with_view_mapper_default_mapper_specified(self):
- from pyramid.response import Response
- response = Response()
- def mapper(**kw):
- def inner(view):
- def superinner(context, request):
- self.assertEqual(request, None)
- return response
- return superinner
- return inner
- self.config.set_view_mapper(mapper)
- def view(context, request): return 'NOTOK'
- deriver = self._makeOne()
- result = deriver(view)
- self.assertFalse(result.__wraps__ is view)
- self.assertEqual(result(None, None), response)
-
- def test_attr_wrapped_view_branching_default_phash(self):
- from pyramid.config.util import DEFAULT_PHASH
- def view(context, request): pass
- deriver = self._makeOne(phash=DEFAULT_PHASH)
- result = deriver(view)
- self.assertEqual(result.__wraps__, view)
-
- def test_attr_wrapped_view_branching_nondefault_phash(self):
- def view(context, request): pass
- deriver = self._makeOne(phash='nondefault')
- result = deriver(view)
- self.assertNotEqual(result, view)
-
- def test_http_cached_view_integer(self):
- import datetime
- from pyramid.response import Response
- response = Response('OK')
- def inner_view(context, request):
- return response
- deriver = self._makeOne(http_cache=3600)
- result = deriver(inner_view)
- self.assertFalse(result is inner_view)
- self.assertEqual(inner_view.__module__, result.__module__)
- self.assertEqual(inner_view.__doc__, result.__doc__)
- request = self._makeRequest()
- when = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
- result = result(None, request)
- self.assertEqual(result, response)
- headers = dict(result.headerlist)
- expires = parse_httpdate(headers['Expires'])
- assert_similar_datetime(expires, when)
- self.assertEqual(headers['Cache-Control'], 'max-age=3600')
-
- def test_http_cached_view_timedelta(self):
- import datetime
- from pyramid.response import Response
- response = Response('OK')
- def inner_view(context, request):
- return response
- deriver = self._makeOne(http_cache=datetime.timedelta(hours=1))
- result = deriver(inner_view)
- self.assertFalse(result is inner_view)
- self.assertEqual(inner_view.__module__, result.__module__)
- self.assertEqual(inner_view.__doc__, result.__doc__)
- request = self._makeRequest()
- when = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
- result = result(None, request)
- self.assertEqual(result, response)
- headers = dict(result.headerlist)
- expires = parse_httpdate(headers['Expires'])
- assert_similar_datetime(expires, when)
- self.assertEqual(headers['Cache-Control'], 'max-age=3600')
-
- def test_http_cached_view_tuple(self):
- import datetime
- from pyramid.response import Response
- response = Response('OK')
- def inner_view(context, request):
- return response
- deriver = self._makeOne(http_cache=(3600, {'public':True}))
- result = deriver(inner_view)
- self.assertFalse(result is inner_view)
- self.assertEqual(inner_view.__module__, result.__module__)
- self.assertEqual(inner_view.__doc__, result.__doc__)
- request = self._makeRequest()
- when = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
- result = result(None, request)
- self.assertEqual(result, response)
- headers = dict(result.headerlist)
- expires = parse_httpdate(headers['Expires'])
- assert_similar_datetime(expires, when)
- self.assertEqual(headers['Cache-Control'], 'max-age=3600, public')
-
- def test_http_cached_view_tuple_seconds_None(self):
- from pyramid.response import Response
- response = Response('OK')
- def inner_view(context, request):
- return response
- deriver = self._makeOne(http_cache=(None, {'public':True}))
- result = deriver(inner_view)
- self.assertFalse(result is inner_view)
- self.assertEqual(inner_view.__module__, result.__module__)
- self.assertEqual(inner_view.__doc__, result.__doc__)
- request = self._makeRequest()
- result = result(None, request)
- self.assertEqual(result, response)
- headers = dict(result.headerlist)
- self.assertFalse('Expires' in headers)
- self.assertEqual(headers['Cache-Control'], 'public')
-
- def test_http_cached_view_prevent_auto_set(self):
- from pyramid.response import Response
- response = Response()
- response.cache_control.prevent_auto = True
- def inner_view(context, request):
- return response
- deriver = self._makeOne(http_cache=3600)
- result = deriver(inner_view)
- request = self._makeRequest()
- result = result(None, request)
- self.assertEqual(result, response) # doesn't blow up
- headers = dict(result.headerlist)
- self.assertFalse('Expires' in headers)
- self.assertFalse('Cache-Control' in headers)
-
- def test_http_cached_prevent_http_cache_in_settings(self):
- self.config.registry.settings['prevent_http_cache'] = True
- from pyramid.response import Response
- response = Response()
- def inner_view(context, request):
- return response
- deriver = self._makeOne(http_cache=3600)
- result = deriver(inner_view)
- request = self._makeRequest()
- result = result(None, request)
- self.assertEqual(result, response)
- headers = dict(result.headerlist)
- self.assertFalse('Expires' in headers)
- self.assertFalse('Cache-Control' in headers)
-
- def test_http_cached_view_bad_tuple(self):
- deriver = self._makeOne(http_cache=(None,))
- def view(request): pass
- self.assertRaises(ConfigurationError, deriver, view)
class TestDefaultViewMapper(unittest.TestCase):
def setUp(self):
@@ -4271,24 +3205,6 @@ class DummyAccept(object):
def __contains__(self, val):
return val in self.matches
-class DummyLogger:
- def __init__(self):
- self.messages = []
- def info(self, msg):
- self.messages.append(msg)
- warn = info
- debug = info
-
-class DummySecurityPolicy:
- def __init__(self, permitted=True):
- self.permitted = permitted
-
- def effective_principals(self, request):
- return []
-
- def permits(self, context, principals, permission):
- return self.permitted
-
class DummyConfig:
def __init__(self):
self.registry = DummyRegistry()
@@ -4379,3 +3295,7 @@ class DummyIntrospector(object):
return self.getval
def relate(self, a, b):
self.related.append((a, b))
+
+class DummySession(dict):
+ def get_csrf_token(self):
+ return self['csrf_token']
diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py
index 2c72c07e8..52e53c34e 100644
--- a/pyramid/tests/test_events.py
+++ b/pyramid/tests/test_events.py
@@ -14,7 +14,7 @@ class NewRequestEventTests(unittest.TestCase):
from zope.interface.verify import verifyClass
klass = self._getTargetClass()
verifyClass(INewRequest, klass)
-
+
def test_instance_conforms_to_INewRequest(self):
from pyramid.interfaces import INewRequest
from zope.interface.verify import verifyObject
@@ -40,7 +40,7 @@ class NewResponseEventTests(unittest.TestCase):
from zope.interface.verify import verifyClass
klass = self._getTargetClass()
verifyClass(INewResponse, klass)
-
+
def test_instance_conforms_to_INewResponse(self):
from pyramid.interfaces import INewResponse
from zope.interface.verify import verifyObject
@@ -103,7 +103,7 @@ class ContextFoundEventTests(unittest.TestCase):
from zope.interface.verify import verifyClass
from pyramid.interfaces import IContextFound
verifyClass(IContextFound, self._getTargetClass())
-
+
def test_instance_conforms_to_IContextFound(self):
from zope.interface.verify import verifyObject
from pyramid.interfaces import IContextFound
@@ -118,12 +118,33 @@ class AfterTraversalEventTests(ContextFoundEventTests):
from zope.interface.verify import verifyClass
from pyramid.interfaces import IAfterTraversal
verifyClass(IAfterTraversal, self._getTargetClass())
-
+
def test_instance_conforms_to_IAfterTraversal(self):
from zope.interface.verify import verifyObject
from pyramid.interfaces import IAfterTraversal
verifyObject(IAfterTraversal, self._makeOne())
+class BeforeTraversalEventTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from pyramid.events import BeforeTraversal
+ return BeforeTraversal
+
+ def _makeOne(self, request=None):
+ if request is None:
+ request = DummyRequest()
+ return self._getTargetClass()(request)
+
+ def test_class_conforms_to_IBeforeTraversal(self):
+ from zope.interface.verify import verifyClass
+ from pyramid.interfaces import IBeforeTraversal
+ verifyClass(IBeforeTraversal, self._getTargetClass())
+
+ def test_instance_conforms_to_IBeforeTraversal(self):
+ from zope.interface.verify import verifyObject
+ from pyramid.interfaces import IBeforeTraversal
+ verifyObject(IBeforeTraversal, self._makeOne())
+
+
class TestSubscriber(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
@@ -221,7 +242,7 @@ class TestBeforeRender(unittest.TestCase):
result = event.setdefault('a', 1)
self.assertEqual(result, 1)
self.assertEqual(event, {'a':1})
-
+
def test_setdefault_success(self):
event = self._makeOne({})
event['a'] = 1
@@ -282,7 +303,7 @@ class DummyConfigurator(object):
class DummyRegistry(object):
pass
-
+
class DummyVenusian(object):
def __init__(self):
self.attached = []
@@ -292,7 +313,7 @@ class DummyVenusian(object):
class Dummy:
pass
-
+
class DummyRequest:
pass
diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py
index b94ef30e4..6c6e16d55 100644
--- a/pyramid/tests/test_httpexceptions.py
+++ b/pyramid/tests/test_httpexceptions.py
@@ -28,9 +28,9 @@ class Test_exception_response(unittest.TestCase):
self.assertTrue(isinstance(self._callFUT(201), HTTPCreated))
def test_extra_kw(self):
- resp = self._callFUT(404, headers=[('abc', 'def')])
+ resp = self._callFUT(404, headers=[('abc', 'def')])
self.assertEqual(resp.headers['abc'], 'def')
-
+
class Test_default_exceptionresponse_view(unittest.TestCase):
def _callFUT(self, context, request):
from pyramid.httpexceptions import default_exceptionresponse_view
@@ -129,7 +129,7 @@ class TestHTTPException(unittest.TestCase):
def test_ctor_sets_body_template_obj(self):
exc = self._makeOne(body_template='${foo}')
self.assertEqual(
- exc.body_template_obj.substitute({'foo':'foo'}), 'foo')
+ exc.body_template_obj.substitute({'foo': 'foo'}), 'foo')
def test_ctor_with_empty_body(self):
cls = self._getTargetSubclass(empty_body=True)
@@ -160,7 +160,7 @@ class TestHTTPException(unittest.TestCase):
self.assertTrue(b'200 OK' in body)
self.assertTrue(b'explanation' in body)
self.assertTrue(b'detail' in body)
-
+
def test_ctor_with_body_sets_default_app_iter_text(self):
cls = self._getTargetSubclass()
exc = cls('detail')
@@ -173,7 +173,7 @@ class TestHTTPException(unittest.TestCase):
exc = self._makeOne()
exc.detail = 'abc'
self.assertEqual(str(exc), 'abc')
-
+
def test__str__explanation(self):
exc = self._makeOne()
exc.explanation = 'def'
@@ -212,6 +212,9 @@ class TestHTTPException(unittest.TestCase):
environ = _makeEnviron()
start_response = DummyStartResponse()
body = list(exc(environ, start_response))[0]
+ for header in start_response.headerlist:
+ if header[0] == 'Content-Type':
+ self.assertEqual(header[1], 'text/plain; charset=UTF-8')
self.assertEqual(body, b'200 OK\n\nexplanation\n\n\n\n\n')
def test__default_app_iter_with_comment_plain(self):
@@ -220,26 +223,78 @@ class TestHTTPException(unittest.TestCase):
environ = _makeEnviron()
start_response = DummyStartResponse()
body = list(exc(environ, start_response))[0]
+ for header in start_response.headerlist:
+ if header[0] == 'Content-Type':
+ self.assertEqual(header[1], 'text/plain; charset=UTF-8')
self.assertEqual(body, b'200 OK\n\nexplanation\n\n\n\ncomment\n')
-
+
def test__default_app_iter_no_comment_html(self):
cls = self._getTargetSubclass()
exc = cls()
environ = _makeEnviron()
start_response = DummyStartResponse()
body = list(exc(environ, start_response))[0]
+ for header in start_response.headerlist:
+ if header[0] == 'Content-Type':
+ self.assertEqual(header[1], 'text/plain; charset=UTF-8')
self.assertFalse(b'<!-- ' in body)
- def test__default_app_iter_with_comment_html(self):
+ def test__content_type(self):
cls = self._getTargetSubclass()
- exc = cls(comment='comment & comment')
+ exc = cls()
+ environ = _makeEnviron()
+ start_response = DummyStartResponse()
+ exc(environ, start_response)
+ for header in start_response.headerlist:
+ if header[0] == 'Content-Type':
+ self.assertEqual(header[1], 'text/plain; charset=UTF-8')
+
+ def test__content_type_default_is_html(self):
+ cls = self._getTargetSubclass()
+ exc = cls()
environ = _makeEnviron()
environ['HTTP_ACCEPT'] = '*/*'
start_response = DummyStartResponse()
+ exc(environ, start_response)
+ for header in start_response.headerlist:
+ if header[0] == 'Content-Type':
+ self.assertEqual(header[1], 'text/html; charset=UTF-8')
+
+ def test__content_type_text_html(self):
+ cls = self._getTargetSubclass()
+ exc = cls()
+ environ = _makeEnviron()
+ environ['HTTP_ACCEPT'] = 'text/html'
+ start_response = DummyStartResponse()
+ exc(environ, start_response)
+ for header in start_response.headerlist:
+ if header[0] == 'Content-Type':
+ self.assertEqual(header[1], 'text/html; charset=UTF-8')
+
+ def test__content_type_application_json(self):
+ cls = self._getTargetSubclass()
+ exc = cls()
+ environ = _makeEnviron()
+ environ['HTTP_ACCEPT'] = 'application/json'
+ start_response = DummyStartResponse()
+ exc(environ, start_response)
+ for header in start_response.headerlist:
+ if header[0] == 'Content-Type':
+ self.assertEqual(header[1], 'application/json')
+
+ def test__default_app_iter_with_comment_ampersand(self):
+ cls = self._getTargetSubclass()
+ exc = cls(comment='comment & comment')
+ environ = _makeEnviron()
+ environ['HTTP_ACCEPT'] = 'text/html'
+ start_response = DummyStartResponse()
body = list(exc(environ, start_response))[0]
+ for header in start_response.headerlist:
+ if header[0] == 'Content-Type':
+ self.assertEqual(header[1], 'text/html; charset=UTF-8')
self.assertTrue(b'<!-- comment &amp; comment -->' in body)
- def test__default_app_iter_with_comment_html2(self):
+ def test__default_app_iter_with_comment_html(self):
cls = self._getTargetSubclass()
exc = cls(comment='comment & comment')
environ = _makeEnviron()
@@ -248,6 +303,38 @@ class TestHTTPException(unittest.TestCase):
body = list(exc(environ, start_response))[0]
self.assertTrue(b'<!-- comment &amp; comment -->' in body)
+ def test__default_app_iter_with_comment_json(self):
+ cls = self._getTargetSubclass()
+ exc = cls(comment='comment & comment')
+ environ = _makeEnviron()
+ environ['HTTP_ACCEPT'] = 'application/json'
+ start_response = DummyStartResponse()
+ body = list(exc(environ, start_response))[0]
+ import json
+ retval = json.loads(body.decode('UTF-8'))
+ self.assertEqual(retval['code'], '200 OK')
+ self.assertEqual(retval['title'], 'OK')
+
+ def test__default_app_iter_with_custom_json(self):
+ def json_formatter(status, body, title, environ):
+ return {'message': body,
+ 'code': status,
+ 'title': title,
+ 'custom': environ['CUSTOM_VARIABLE']
+ }
+ cls = self._getTargetSubclass()
+ exc = cls(comment='comment', json_formatter=json_formatter)
+ environ = _makeEnviron()
+ environ['HTTP_ACCEPT'] = 'application/json'
+ environ['CUSTOM_VARIABLE'] = 'custom!'
+ start_response = DummyStartResponse()
+ body = list(exc(environ, start_response))[0]
+ import json
+ retval = json.loads(body.decode('UTF-8'))
+ self.assertEqual(retval['code'], '200 OK')
+ self.assertEqual(retval['title'], 'OK')
+ self.assertEqual(retval['custom'], 'custom!')
+
def test_custom_body_template(self):
cls = self._getTargetSubclass()
exc = cls(body_template='${REQUEST_METHOD}')
@@ -261,7 +348,8 @@ class TestHTTPException(unittest.TestCase):
exc = cls(body_template='${REQUEST_METHOD}')
environ = _makeEnviron()
class Choke(object):
- def __str__(self): raise ValueError
+ def __str__(self): # pragma nocover
+ raise ValueError
environ['gardentheory.user'] = Choke()
start_response = DummyStartResponse()
body = list(exc(environ, start_response))[0]
@@ -293,7 +381,7 @@ class TestRenderAllExceptionsWithoutArguments(unittest.TestCase):
self.assertTrue(bytes_(exc.status) in result)
L.append(result)
self.assertEqual(len(L), len(status_map))
-
+
def test_it_plain(self):
self._doit('text/plain')
@@ -367,12 +455,11 @@ class DummyStartResponse(object):
def __call__(self, status, headerlist):
self.status = status
self.headerlist = headerlist
-
+
def _makeEnviron(**kw):
- environ = {'REQUEST_METHOD':'GET',
- 'wsgi.url_scheme':'http',
- 'SERVER_NAME':'localhost',
- 'SERVER_PORT':'80'}
+ environ = {'REQUEST_METHOD': 'GET',
+ 'wsgi.url_scheme': 'http',
+ 'SERVER_NAME': 'localhost',
+ 'SERVER_PORT': '80'}
environ.update(kw)
return environ
-
diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py
index 5e341172c..22a5cde3d 100644
--- a/pyramid/tests/test_paster.py
+++ b/pyramid/tests/test_paster.py
@@ -105,18 +105,38 @@ class Test_get_appsettings(unittest.TestCase):
self.assertEqual(result['foo'], 'baz')
class Test_setup_logging(unittest.TestCase):
- def _callFUT(self, config_file):
+ def _callFUT(self, config_file, global_conf=None):
from pyramid.paster import setup_logging
dummy_cp = DummyConfigParserModule
- return setup_logging(config_file, self.fileConfig, dummy_cp)
+ return setup_logging(
+ config_uri=config_file,
+ global_conf=global_conf,
+ fileConfig=self.fileConfig,
+ configparser=dummy_cp,
+ )
- def test_it(self):
+ def test_it_no_global_conf(self):
config_file, dict = self._callFUT('/abc')
# os.path.abspath is a sop to Windows
self.assertEqual(config_file, os.path.abspath('/abc'))
self.assertEqual(dict['__file__'], os.path.abspath('/abc'))
self.assertEqual(dict['here'], os.path.abspath('/'))
+ def test_it_global_conf_empty(self):
+ config_file, dict = self._callFUT('/abc', global_conf={})
+ # os.path.abspath is a sop to Windows
+ self.assertEqual(config_file, os.path.abspath('/abc'))
+ self.assertEqual(dict['__file__'], os.path.abspath('/abc'))
+ self.assertEqual(dict['here'], os.path.abspath('/'))
+
+ def test_it_global_conf_not_empty(self):
+ config_file, dict = self._callFUT('/abc', global_conf={'key': 'val'})
+ # os.path.abspath is a sop to Windows
+ self.assertEqual(config_file, os.path.abspath('/abc'))
+ self.assertEqual(dict['__file__'], os.path.abspath('/abc'))
+ self.assertEqual(dict['here'], os.path.abspath('/'))
+ self.assertEqual(dict['key'], 'val')
+
def fileConfig(self, config_file, dict):
return config_file, dict
diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py
index 2458ea830..65bfa5582 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -592,48 +592,6 @@ class Test_render_to_response(unittest.TestCase):
self.assertEqual(result.body, b'{"a": 1}')
self.assertFalse('response' in request.__dict__)
-class Test_temporary_response(unittest.TestCase):
- def _callFUT(self, request):
- from pyramid.renderers import temporary_response
- return temporary_response(request)
-
- def test_restores_response(self):
- request = testing.DummyRequest()
- orig_response = request.response
- with self._callFUT(request):
- request.response = object()
- self.assertEqual(request.response, orig_response)
-
- def test_restores_response_on_exception(self):
- request = testing.DummyRequest()
- orig_response = request.response
- try:
- with self._callFUT(request):
- request.response = object()
- raise RuntimeError()
- except RuntimeError:
- self.assertEqual(request.response, orig_response)
- else: # pragma: no cover
- self.fail("RuntimeError not raised")
-
- def test_restores_response_to_none(self):
- request = testing.DummyRequest(response=None)
- with self._callFUT(request):
- request.response = object()
- self.assertEqual(request.response, None)
-
- def test_deletes_response(self):
- request = testing.DummyRequest()
- with self._callFUT(request):
- request.response = object()
- self.assertTrue('response' not in request.__dict__)
-
- def test_does_not_delete_response_if_no_response_to_delete(self):
- request = testing.DummyRequest()
- with self._callFUT(request):
- pass
- self.assertTrue('response' not in request.__dict__)
-
class Test_get_renderer(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py
index 1cdc4abaa..7aa42804c 100644
--- a/pyramid/tests/test_router.py
+++ b/pyramid/tests/test_router.py
@@ -591,6 +591,7 @@ class TestRouter(unittest.TestCase):
def test_call_eventsends(self):
from pyramid.interfaces import INewRequest
from pyramid.interfaces import INewResponse
+ from pyramid.interfaces import IBeforeTraversal
from pyramid.interfaces import IContextFound
from pyramid.interfaces import IViewClassifier
context = DummyContext()
@@ -601,6 +602,7 @@ class TestRouter(unittest.TestCase):
environ = self._makeEnviron()
self._registerView(view, '', IViewClassifier, None, None)
request_events = self._registerEventListener(INewRequest)
+ beforetraversal_events = self._registerEventListener(IBeforeTraversal)
context_found_events = self._registerEventListener(IContextFound)
response_events = self._registerEventListener(INewResponse)
router = self._makeOne()
@@ -608,6 +610,8 @@ class TestRouter(unittest.TestCase):
result = router(environ, start_response)
self.assertEqual(len(request_events), 1)
self.assertEqual(request_events[0].request.environ, environ)
+ self.assertEqual(len(beforetraversal_events), 1)
+ self.assertEqual(beforetraversal_events[0].request.environ, environ)
self.assertEqual(len(context_found_events), 1)
self.assertEqual(context_found_events[0].request.environ, environ)
self.assertEqual(context_found_events[0].request.context, context)
diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py
index 914d28a83..e08f9a919 100644
--- a/pyramid/tests/test_session.py
+++ b/pyramid/tests/test_session.py
@@ -666,7 +666,8 @@ class Test_check_csrf_token(unittest.TestCase):
def test_success_token(self):
request = testing.DummyRequest()
- request.params['csrf_token'] = request.session.get_csrf_token()
+ request.method = "POST"
+ request.POST = {'csrf_token': request.session.get_csrf_token()}
self.assertEqual(self._callFUT(request, token='csrf_token'), True)
def test_success_header(self):
@@ -676,7 +677,8 @@ class Test_check_csrf_token(unittest.TestCase):
def test_success_default_token(self):
request = testing.DummyRequest()
- request.params['csrf_token'] = request.session.get_csrf_token()
+ request.method = "POST"
+ request.POST = {'csrf_token': request.session.get_csrf_token()}
self.assertEqual(self._callFUT(request), True)
def test_success_default_header(self):
@@ -698,10 +700,104 @@ class Test_check_csrf_token(unittest.TestCase):
def test_token_differing_types(self):
from pyramid.compat import text_
request = testing.DummyRequest()
+ request.method = "POST"
request.session['_csrft_'] = text_('foo')
- request.params['csrf_token'] = b'foo'
+ request.POST = {'csrf_token': b'foo'}
self.assertEqual(self._callFUT(request, token='csrf_token'), True)
+
+class Test_check_csrf_origin(unittest.TestCase):
+
+ def _callFUT(self, *args, **kwargs):
+ from ..session import check_csrf_origin
+ return check_csrf_origin(*args, **kwargs)
+
+ def test_success_with_http(self):
+ request = testing.DummyRequest()
+ request.scheme = "http"
+ self.assertTrue(self._callFUT(request))
+
+ def test_success_with_https_and_referrer(self):
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = 443
+ request.referrer = "https://example.com/login/"
+ request.registry.settings = {}
+ self.assertTrue(self._callFUT(request))
+
+ def test_success_with_https_and_origin(self):
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = 443
+ request.headers = {"Origin": "https://example.com/"}
+ request.referrer = "https://not-example.com/"
+ request.registry.settings = {}
+ self.assertTrue(self._callFUT(request))
+
+ def test_success_with_additional_trusted_host(self):
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = 443
+ request.referrer = "https://not-example.com/login/"
+ request.registry.settings = {
+ "pyramid.csrf_trusted_origins": ["not-example.com"],
+ }
+ self.assertTrue(self._callFUT(request))
+
+ def test_success_with_nonstandard_port(self):
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com:8080"
+ request.host_port = 8080
+ request.referrer = "https://example.com:8080/login/"
+ request.registry.settings = {}
+ self.assertTrue(self._callFUT(request))
+
+ def test_fails_with_wrong_host(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = 443
+ request.referrer = "https://not-example.com/login/"
+ request.registry.settings = {}
+ self.assertRaises(BadCSRFOrigin, self._callFUT, request)
+ self.assertFalse(self._callFUT(request, raises=False))
+
+ def test_fails_with_no_origin(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.referrer = None
+ self.assertRaises(BadCSRFOrigin, self._callFUT, request)
+ self.assertFalse(self._callFUT(request, raises=False))
+
+ def test_fails_when_http_to_https(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com"
+ request.host_port = 443
+ request.referrer = "http://example.com/evil/"
+ request.registry.settings = {}
+ self.assertRaises(BadCSRFOrigin, self._callFUT, request)
+ self.assertFalse(self._callFUT(request, raises=False))
+
+ def test_fails_with_nonstandard_port(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ request = testing.DummyRequest()
+ request.scheme = "https"
+ request.host = "example.com:8080"
+ request.host_port = 8080
+ request.referrer = "https://example.com/login/"
+ request.registry.settings = {}
+ self.assertRaises(BadCSRFOrigin, self._callFUT, request)
+ self.assertFalse(self._callFUT(request, raises=False))
+
+
class DummySerializer(object):
def dumps(self, value):
return base64.b64encode(json.dumps(value).encode('utf-8'))
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index 0be99e949..bbf6103f4 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -794,8 +794,86 @@ class TestCallableName(unittest.TestCase):
self.assertRaises(ConfigurationError, get_bad_name)
+class Test_hide_attrs(unittest.TestCase):
+ def _callFUT(self, obj, *attrs):
+ from pyramid.util import hide_attrs
+ return hide_attrs(obj, *attrs)
+
+ def _makeDummy(self):
+ from pyramid.decorator import reify
+ class Dummy(object):
+ x = 1
+
+ @reify
+ def foo(self):
+ return self.x
+ return Dummy()
+
+ def test_restores_attrs(self):
+ obj = self._makeDummy()
+ obj.bar = 'asdf'
+ orig_foo = obj.foo
+ with self._callFUT(obj, 'foo', 'bar'):
+ obj.foo = object()
+ obj.bar = 'nope'
+ self.assertEqual(obj.foo, orig_foo)
+ self.assertEqual(obj.bar, 'asdf')
+
+ def test_restores_attrs_on_exception(self):
+ obj = self._makeDummy()
+ orig_foo = obj.foo
+ try:
+ with self._callFUT(obj, 'foo'):
+ obj.foo = object()
+ raise RuntimeError()
+ except RuntimeError:
+ self.assertEqual(obj.foo, orig_foo)
+ else: # pragma: no cover
+ self.fail("RuntimeError not raised")
+
+ def test_restores_attrs_to_none(self):
+ obj = self._makeDummy()
+ obj.foo = None
+ with self._callFUT(obj, 'foo'):
+ obj.foo = object()
+ self.assertEqual(obj.foo, None)
+
+ def test_deletes_attrs(self):
+ obj = self._makeDummy()
+ with self._callFUT(obj, 'foo'):
+ obj.foo = object()
+ self.assertTrue('foo' not in obj.__dict__)
+
+ def test_does_not_delete_attr_if_no_attr_to_delete(self):
+ obj = self._makeDummy()
+ with self._callFUT(obj, 'foo'):
+ pass
+ self.assertTrue('foo' not in obj.__dict__)
+
+
def dummyfunc(): pass
class Dummy(object):
pass
+
+
+class Test_is_same_domain(unittest.TestCase):
+ def _callFUT(self, *args, **kw):
+ from pyramid.util import is_same_domain
+ return is_same_domain(*args, **kw)
+
+ def test_it(self):
+ self.assertTrue(self._callFUT("example.com", "example.com"))
+ self.assertFalse(self._callFUT("evil.com", "example.com"))
+ self.assertFalse(self._callFUT("evil.example.com", "example.com"))
+ self.assertFalse(self._callFUT("example.com", ""))
+
+ def test_with_wildcard(self):
+ self.assertTrue(self._callFUT("example.com", ".example.com"))
+ self.assertTrue(self._callFUT("good.example.com", ".example.com"))
+
+ def test_with_port(self):
+ self.assertTrue(self._callFUT("example.com:8080", "example.com:8080"))
+ self.assertFalse(self._callFUT("example.com:8080", "example.com"))
+ self.assertFalse(self._callFUT("example.com", "example.com:8080"))
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index e6b9f9e7e..2be47e318 100644
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -673,6 +673,138 @@ class Test_view_defaults(unittest.TestCase):
class Bar(Foo): pass
self.assertEqual(Bar.__view_defaults__, {})
+class TestViewMethodsMixin(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _makeOne(self, environ=None):
+ from pyramid.decorator import reify
+ from pyramid.view import ViewMethodsMixin
+ if environ is None:
+ environ = {}
+ class Request(ViewMethodsMixin):
+ def __init__(self, environ):
+ self.environ = environ
+
+ @reify
+ def response(self):
+ return DummyResponse()
+ request = Request(environ)
+ request.registry = self.config.registry
+ return request
+
+ def test_it(self):
+ def exc_view(exc, request):
+ self.assertTrue(exc is dummy_exc)
+ self.assertTrue(request.exception is dummy_exc)
+ return DummyResponse(b'foo')
+ self.config.add_view(exc_view, context=RuntimeError)
+ request = self._makeOne()
+ dummy_exc = RuntimeError()
+ try:
+ raise dummy_exc
+ except RuntimeError:
+ response = request.invoke_exception_view()
+ self.assertEqual(response.app_iter, [b'foo'])
+ else: # pragma: no cover
+ self.fail()
+
+ def test_it_hides_attrs(self):
+ def exc_view(exc, request):
+ self.assertTrue(exc is not orig_exc)
+ self.assertTrue(request.exception is not orig_exc)
+ self.assertTrue(request.exc_info is not orig_exc_info)
+ self.assertTrue(request.response is not orig_response)
+ request.response.app_iter = [b'bar']
+ return request.response
+ self.config.add_view(exc_view, context=RuntimeError)
+ request = self._makeOne()
+ orig_exc = request.exception = DummyContext()
+ orig_exc_info = request.exc_info = DummyContext()
+ orig_response = request.response = DummyResponse(b'foo')
+ try:
+ raise RuntimeError
+ except RuntimeError:
+ response = request.invoke_exception_view()
+ self.assertEqual(response.app_iter, [b'bar'])
+ self.assertTrue(request.exception is orig_exc)
+ self.assertTrue(request.exc_info is orig_exc_info)
+ self.assertTrue(request.response is orig_response)
+ else: # pragma: no cover
+ self.fail()
+
+ def test_it_supports_alternate_requests(self):
+ def exc_view(exc, request):
+ self.assertTrue(request is other_req)
+ return DummyResponse(b'foo')
+ self.config.add_view(exc_view, context=RuntimeError)
+ request = self._makeOne()
+ other_req = self._makeOne()
+ try:
+ raise RuntimeError
+ except RuntimeError:
+ response = request.invoke_exception_view(request=other_req)
+ self.assertEqual(response.app_iter, [b'foo'])
+ else: # pragma: no cover
+ self.fail()
+
+ def test_it_supports_threadlocal_registry(self):
+ def exc_view(exc, request):
+ return DummyResponse(b'foo')
+ self.config.add_view(exc_view, context=RuntimeError)
+ request = self._makeOne()
+ del request.registry
+ try:
+ raise RuntimeError
+ except RuntimeError:
+ response = request.invoke_exception_view()
+ self.assertEqual(response.app_iter, [b'foo'])
+ else: # pragma: no cover
+ self.fail()
+
+ def test_it_supports_alternate_exc_info(self):
+ def exc_view(exc, request):
+ self.assertTrue(request.exc_info is exc_info)
+ return DummyResponse(b'foo')
+ self.config.add_view(exc_view, context=RuntimeError)
+ request = self._makeOne()
+ try:
+ raise RuntimeError
+ except RuntimeError:
+ exc_info = sys.exc_info()
+ response = request.invoke_exception_view(exc_info=exc_info)
+ self.assertEqual(response.app_iter, [b'foo'])
+
+ def test_it_rejects_secured_view(self):
+ from pyramid.exceptions import Forbidden
+ def exc_view(exc, request): pass
+ self.config.testing_securitypolicy(permissive=False)
+ self.config.add_view(exc_view, context=RuntimeError, permission='view')
+ request = self._makeOne()
+ try:
+ raise RuntimeError
+ except RuntimeError:
+ self.assertRaises(Forbidden, request.invoke_exception_view)
+ else: # pragma: no cover
+ self.fail()
+
+ def test_it_allows_secured_view(self):
+ def exc_view(exc, request):
+ return DummyResponse(b'foo')
+ self.config.testing_securitypolicy(permissive=False)
+ self.config.add_view(exc_view, context=RuntimeError, permission='view')
+ request = self._makeOne()
+ try:
+ raise RuntimeError
+ except RuntimeError:
+ response = request.invoke_exception_view(secure=False)
+ self.assertEqual(response.app_iter, [b'foo'])
+ else: # pragma: no cover
+ self.fail()
+
class ExceptionResponse(Exception):
status = '404 Not Found'
app_iter = ['Not Found']
diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py
new file mode 100644
index 000000000..6bfe353e5
--- /dev/null
+++ b/pyramid/tests/test_viewderivers.py
@@ -0,0 +1,1658 @@
+import unittest
+from zope.interface import implementer
+
+from pyramid import testing
+from pyramid.exceptions import ConfigurationError
+from pyramid.interfaces import (
+ IResponse,
+ IRequest,
+ )
+
+class TestDeriveView(unittest.TestCase):
+
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ self.config = None
+ testing.tearDown()
+
+ def _makeRequest(self):
+ request = DummyRequest()
+ request.registry = self.config.registry
+ return request
+
+ def _registerLogger(self):
+ from pyramid.interfaces import IDebugLogger
+ logger = DummyLogger()
+ self.config.registry.registerUtility(logger, IDebugLogger)
+ return logger
+
+ def _registerSecurityPolicy(self, permissive):
+ from pyramid.interfaces import IAuthenticationPolicy
+ from pyramid.interfaces import IAuthorizationPolicy
+ policy = DummySecurityPolicy(permissive)
+ self.config.registry.registerUtility(policy, IAuthenticationPolicy)
+ self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+
+ def test_function_returns_non_adaptable(self):
+ def view(request):
+ return None
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ try:
+ result(None, None)
+ except ValueError as e:
+ self.assertEqual(
+ e.args[0],
+ 'Could not convert return value of the view callable function '
+ 'pyramid.tests.test_viewderivers.view into a response '
+ 'object. The value returned was None. You may have forgotten '
+ 'to return a value from the view callable.'
+ )
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_function_returns_non_adaptable_dict(self):
+ def view(request):
+ return {'a':1}
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ try:
+ result(None, None)
+ except ValueError as e:
+ self.assertEqual(
+ e.args[0],
+ "Could not convert return value of the view callable function "
+ "pyramid.tests.test_viewderivers.view into a response "
+ "object. The value returned was {'a': 1}. You may have "
+ "forgotten to define a renderer in the view configuration."
+ )
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_instance_returns_non_adaptable(self):
+ class AView(object):
+ def __call__(self, request):
+ return None
+ view = AView()
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ try:
+ result(None, None)
+ except ValueError as e:
+ msg = e.args[0]
+ self.assertTrue(msg.startswith(
+ 'Could not convert return value of the view callable object '
+ '<pyramid.tests.test_viewderivers.'))
+ self.assertTrue(msg.endswith(
+ '> into a response object. The value returned was None. You '
+ 'may have forgotten to return a value from the view callable.'))
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_function_returns_true_Response_no_renderer(self):
+ from pyramid.response import Response
+ r = Response('Hello')
+ def view(request):
+ return r
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ response = result(None, None)
+ self.assertEqual(response, r)
+
+ def test_function_returns_true_Response_with_renderer(self):
+ from pyramid.response import Response
+ r = Response('Hello')
+ def view(request):
+ return r
+ renderer = object()
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ response = result(None, None)
+ self.assertEqual(response, r)
+
+ def test_requestonly_default_method_returns_non_adaptable(self):
+ request = DummyRequest()
+ class AView(object):
+ def __init__(self, request):
+ pass
+ def __call__(self):
+ return None
+ result = self.config.derive_view(AView)
+ self.assertFalse(result is AView)
+ try:
+ result(None, request)
+ except ValueError as e:
+ self.assertEqual(
+ e.args[0],
+ 'Could not convert return value of the view callable '
+ 'method __call__ of '
+ 'class pyramid.tests.test_viewderivers.AView into a '
+ 'response object. The value returned was None. You may have '
+ 'forgotten to return a value from the view callable.'
+ )
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_requestonly_nondefault_method_returns_non_adaptable(self):
+ request = DummyRequest()
+ class AView(object):
+ def __init__(self, request):
+ pass
+ def theviewmethod(self):
+ return None
+ result = self.config.derive_view(AView, attr='theviewmethod')
+ self.assertFalse(result is AView)
+ try:
+ result(None, request)
+ except ValueError as e:
+ self.assertEqual(
+ e.args[0],
+ 'Could not convert return value of the view callable '
+ 'method theviewmethod of '
+ 'class pyramid.tests.test_viewderivers.AView into a '
+ 'response object. The value returned was None. You may have '
+ 'forgotten to return a value from the view callable.'
+ )
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_requestonly_function(self):
+ response = DummyResponse()
+ def view(request):
+ return response
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ self.assertEqual(result(None, None), response)
+
+ def test_requestonly_function_with_renderer(self):
+ response = DummyResponse()
+ class moo(object):
+ def render_view(inself, req, resp, view_inst, ctx):
+ self.assertEqual(req, request)
+ self.assertEqual(resp, 'OK')
+ self.assertEqual(view_inst, view)
+ self.assertEqual(ctx, context)
+ return response
+ def clone(self):
+ return self
+ def view(request):
+ return 'OK'
+ result = self.config.derive_view(view, renderer=moo())
+ self.assertFalse(result.__wraps__ is view)
+ request = self._makeRequest()
+ context = testing.DummyResource()
+ self.assertEqual(result(context, request), response)
+
+ def test_requestonly_function_with_renderer_request_override(self):
+ def moo(info):
+ def inner(value, system):
+ self.assertEqual(value, 'OK')
+ self.assertEqual(system['request'], request)
+ self.assertEqual(system['context'], context)
+ return b'moo'
+ return inner
+ def view(request):
+ return 'OK'
+ self.config.add_renderer('moo', moo)
+ result = self.config.derive_view(view, renderer='string')
+ self.assertFalse(result is view)
+ request = self._makeRequest()
+ request.override_renderer = 'moo'
+ context = testing.DummyResource()
+ self.assertEqual(result(context, request).body, b'moo')
+
+ def test_requestonly_function_with_renderer_request_has_view(self):
+ response = DummyResponse()
+ class moo(object):
+ def render_view(inself, req, resp, view_inst, ctx):
+ self.assertEqual(req, request)
+ self.assertEqual(resp, 'OK')
+ self.assertEqual(view_inst, 'view')
+ self.assertEqual(ctx, context)
+ return response
+ def clone(self):
+ return self
+ def view(request):
+ return 'OK'
+ result = self.config.derive_view(view, renderer=moo())
+ self.assertFalse(result.__wraps__ is view)
+ request = self._makeRequest()
+ request.__view__ = 'view'
+ context = testing.DummyResource()
+ r = result(context, request)
+ self.assertEqual(r, response)
+ self.assertFalse(hasattr(request, '__view__'))
+
+ def test_class_without_attr(self):
+ response = DummyResponse()
+ class View(object):
+ def __init__(self, request):
+ pass
+ def __call__(self):
+ return response
+ result = self.config.derive_view(View)
+ request = self._makeRequest()
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(request.__view__.__class__, View)
+
+ def test_class_with_attr(self):
+ response = DummyResponse()
+ class View(object):
+ def __init__(self, request):
+ pass
+ def another(self):
+ return response
+ result = self.config.derive_view(View, attr='another')
+ request = self._makeRequest()
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(request.__view__.__class__, View)
+
+ def test_as_function_context_and_request(self):
+ def view(context, request):
+ return 'OK'
+ result = self.config.derive_view(view)
+ self.assertTrue(result.__wraps__ is view)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ self.assertEqual(view(None, None), 'OK')
+
+ def test_as_function_requestonly(self):
+ response = DummyResponse()
+ def view(request):
+ return response
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ self.assertEqual(result(None, None), response)
+
+ def test_as_newstyle_class_context_and_request(self):
+ response = DummyResponse()
+ class view(object):
+ def __init__(self, context, request):
+ pass
+ def __call__(self):
+ return response
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(request.__view__.__class__, view)
+
+ def test_as_newstyle_class_requestonly(self):
+ response = DummyResponse()
+ class view(object):
+ def __init__(self, context, request):
+ pass
+ def __call__(self):
+ return response
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(request.__view__.__class__, view)
+
+ def test_as_oldstyle_class_context_and_request(self):
+ response = DummyResponse()
+ class view:
+ def __init__(self, context, request):
+ pass
+ def __call__(self):
+ return response
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(request.__view__.__class__, view)
+
+ def test_as_oldstyle_class_requestonly(self):
+ response = DummyResponse()
+ class view:
+ def __init__(self, context, request):
+ pass
+ def __call__(self):
+ return response
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(request.__view__.__class__, view)
+
+ def test_as_instance_context_and_request(self):
+ response = DummyResponse()
+ class View:
+ def __call__(self, context, request):
+ return response
+ view = View()
+ result = self.config.derive_view(view)
+ self.assertTrue(result.__wraps__ is view)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ self.assertEqual(result(None, None), response)
+
+ def test_as_instance_requestonly(self):
+ response = DummyResponse()
+ class View:
+ def __call__(self, request):
+ return response
+ view = View()
+ result = self.config.derive_view(view)
+ self.assertFalse(result is view)
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertTrue('test_viewderivers' in result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ self.assertEqual(result(None, None), response)
+
+ def test_with_debug_authorization_no_authpol(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ logger = self._registerLogger()
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context None): Allowed "
+ "(no authorization policy in use)")
+
+ def test_with_debug_authorization_authn_policy_no_authz_policy(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(debug_authorization=True)
+ from pyramid.interfaces import IAuthenticationPolicy
+ policy = DummySecurityPolicy(False)
+ self.config.registry.registerUtility(policy, IAuthenticationPolicy)
+ logger = self._registerLogger()
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context None): Allowed "
+ "(no authorization policy in use)")
+
+ def test_with_debug_authorization_authz_policy_no_authn_policy(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(debug_authorization=True)
+ from pyramid.interfaces import IAuthorizationPolicy
+ policy = DummySecurityPolicy(False)
+ self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+ logger = self._registerLogger()
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context None): Allowed "
+ "(no authorization policy in use)")
+
+ def test_with_debug_authorization_no_permission(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ self._registerSecurityPolicy(True)
+ logger = self._registerLogger()
+ result = self.config._derive_view(view)
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context None): Allowed ("
+ "no permission registered)")
+
+ def test_debug_auth_permission_authpol_permitted(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ logger = self._registerLogger()
+ self._registerSecurityPolicy(True)
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertEqual(result.__call_permissive__.__wraps__, view)
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context None): True")
+
+ def test_debug_auth_permission_authpol_permitted_no_request(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ logger = self._registerLogger()
+ self._registerSecurityPolicy(True)
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertEqual(result.__call_permissive__.__wraps__, view)
+ self.assertEqual(result(None, None), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url None (view name "
+ "None against context None): True")
+
+ def test_debug_auth_permission_authpol_denied(self):
+ from pyramid.httpexceptions import HTTPForbidden
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ logger = self._registerLogger()
+ self._registerSecurityPolicy(False)
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertEqual(result.__call_permissive__.__wraps__, view)
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertRaises(HTTPForbidden, result, None, request)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context None): False")
+
+ def test_debug_auth_permission_authpol_denied2(self):
+ view = lambda *arg: 'OK'
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ self._registerLogger()
+ self._registerSecurityPolicy(False)
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ permitted = result.__permitted__(None, None)
+ self.assertEqual(permitted, False)
+
+ def test_debug_auth_permission_authpol_overridden(self):
+ from pyramid.security import NO_PERMISSION_REQUIRED
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ logger = self._registerLogger()
+ self._registerSecurityPolicy(False)
+ result = self.config._derive_view(view, permission=NO_PERMISSION_REQUIRED)
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(None, request), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context None): "
+ "Allowed (NO_PERMISSION_REQUIRED)")
+
+ def test_secured_view_authn_policy_no_authz_policy(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = {}
+ from pyramid.interfaces import IAuthenticationPolicy
+ policy = DummySecurityPolicy(False)
+ self.config.registry.registerUtility(policy, IAuthenticationPolicy)
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(None, request), response)
+
+ def test_secured_view_authz_policy_no_authn_policy(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = {}
+ from pyramid.interfaces import IAuthorizationPolicy
+ policy = DummySecurityPolicy(False)
+ self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+ result = self.config._derive_view(view, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertFalse(hasattr(result, '__call_permissive__'))
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(None, request), response)
+
+ def test_secured_view_raises_forbidden_no_name(self):
+ from pyramid.interfaces import IAuthenticationPolicy
+ from pyramid.interfaces import IAuthorizationPolicy
+ from pyramid.httpexceptions import HTTPForbidden
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = {}
+ policy = DummySecurityPolicy(False)
+ self.config.registry.registerUtility(policy, IAuthenticationPolicy)
+ self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+ result = self.config._derive_view(view, permission='view')
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ try:
+ result(None, request)
+ except HTTPForbidden as e:
+ self.assertEqual(e.message,
+ 'Unauthorized: <lambda> failed permission check')
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_secured_view_raises_forbidden_with_name(self):
+ from pyramid.interfaces import IAuthenticationPolicy
+ from pyramid.interfaces import IAuthorizationPolicy
+ from pyramid.httpexceptions import HTTPForbidden
+ def myview(request): pass
+ self.config.registry.settings = {}
+ policy = DummySecurityPolicy(False)
+ self.config.registry.registerUtility(policy, IAuthenticationPolicy)
+ self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+ result = self.config._derive_view(myview, permission='view')
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ try:
+ result(None, request)
+ except HTTPForbidden as e:
+ self.assertEqual(e.message,
+ 'Unauthorized: myview failed permission check')
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_predicate_mismatch_view_has_no_name(self):
+ from pyramid.exceptions import PredicateMismatch
+ response = DummyResponse()
+ view = lambda *arg: response
+ def predicate1(context, request):
+ return False
+ predicate1.text = lambda *arg: 'text'
+ result = self.config._derive_view(view, predicates=[predicate1])
+ request = self._makeRequest()
+ request.method = 'POST'
+ try:
+ result(None, None)
+ except PredicateMismatch as e:
+ self.assertEqual(e.detail,
+ 'predicate mismatch for view <lambda> (text)')
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_predicate_mismatch_view_has_name(self):
+ from pyramid.exceptions import PredicateMismatch
+ def myview(request): pass
+ def predicate1(context, request):
+ return False
+ predicate1.text = lambda *arg: 'text'
+ result = self.config._derive_view(myview, predicates=[predicate1])
+ request = self._makeRequest()
+ request.method = 'POST'
+ try:
+ result(None, None)
+ except PredicateMismatch as e:
+ self.assertEqual(e.detail,
+ 'predicate mismatch for view myview (text)')
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_predicate_mismatch_exception_has_text_in_detail(self):
+ from pyramid.exceptions import PredicateMismatch
+ def myview(request): pass
+ def predicate1(context, request):
+ return True
+ predicate1.text = lambda *arg: 'pred1'
+ def predicate2(context, request):
+ return False
+ predicate2.text = lambda *arg: 'pred2'
+ result = self.config._derive_view(myview,
+ predicates=[predicate1, predicate2])
+ request = self._makeRequest()
+ request.method = 'POST'
+ try:
+ result(None, None)
+ except PredicateMismatch as e:
+ self.assertEqual(e.detail,
+ 'predicate mismatch for view myview (pred2)')
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_with_predicates_all(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ predicates = []
+ def predicate1(context, request):
+ predicates.append(True)
+ return True
+ def predicate2(context, request):
+ predicates.append(True)
+ return True
+ result = self.config._derive_view(view,
+ predicates=[predicate1, predicate2])
+ request = self._makeRequest()
+ request.method = 'POST'
+ next = result(None, None)
+ self.assertEqual(next, response)
+ self.assertEqual(predicates, [True, True])
+
+ def test_with_predicates_checker(self):
+ view = lambda *arg: 'OK'
+ predicates = []
+ def predicate1(context, request):
+ predicates.append(True)
+ return True
+ def predicate2(context, request):
+ predicates.append(True)
+ return True
+ result = self.config._derive_view(view,
+ predicates=[predicate1, predicate2])
+ request = self._makeRequest()
+ request.method = 'POST'
+ next = result.__predicated__(None, None)
+ self.assertEqual(next, True)
+ self.assertEqual(predicates, [True, True])
+
+ def test_with_predicates_notall(self):
+ from pyramid.httpexceptions import HTTPNotFound
+ view = lambda *arg: 'OK'
+ predicates = []
+ def predicate1(context, request):
+ predicates.append(True)
+ return True
+ predicate1.text = lambda *arg: 'text'
+ def predicate2(context, request):
+ predicates.append(True)
+ return False
+ predicate2.text = lambda *arg: 'text'
+ result = self.config._derive_view(view,
+ predicates=[predicate1, predicate2])
+ request = self._makeRequest()
+ request.method = 'POST'
+ self.assertRaises(HTTPNotFound, result, None, None)
+ self.assertEqual(predicates, [True, True])
+
+ def test_with_wrapper_viewname(self):
+ from pyramid.response import Response
+ from pyramid.interfaces import IView
+ from pyramid.interfaces import IViewClassifier
+ inner_response = Response('OK')
+ def inner_view(context, request):
+ return inner_response
+ def outer_view(context, request):
+ self.assertEqual(request.wrapped_response, inner_response)
+ self.assertEqual(request.wrapped_body, inner_response.body)
+ self.assertEqual(request.wrapped_view.__original_view__,
+ inner_view)
+ return Response(b'outer ' + request.wrapped_body)
+ self.config.registry.registerAdapter(
+ outer_view, (IViewClassifier, None, None), IView, 'owrap')
+ result = self.config._derive_view(inner_view, viewname='inner',
+ wrapper_viewname='owrap')
+ self.assertFalse(result is inner_view)
+ self.assertEqual(inner_view.__module__, result.__module__)
+ self.assertEqual(inner_view.__doc__, result.__doc__)
+ request = self._makeRequest()
+ response = result(None, request)
+ self.assertEqual(response.body, b'outer OK')
+
+ def test_with_wrapper_viewname_notfound(self):
+ from pyramid.response import Response
+ inner_response = Response('OK')
+ def inner_view(context, request):
+ return inner_response
+ wrapped = self.config._derive_view(inner_view, viewname='inner',
+ wrapper_viewname='owrap')
+ request = self._makeRequest()
+ self.assertRaises(ValueError, wrapped, None, request)
+
+ def test_as_newstyle_class_context_and_request_attr_and_renderer(self):
+ response = DummyResponse()
+ class renderer(object):
+ def render_view(inself, req, resp, view_inst, ctx):
+ self.assertEqual(req, request)
+ self.assertEqual(resp, {'a':'1'})
+ self.assertEqual(view_inst.__class__, View)
+ self.assertEqual(ctx, context)
+ return response
+ def clone(self):
+ return self
+ class View(object):
+ def __init__(self, context, request):
+ pass
+ def index(self):
+ return {'a':'1'}
+ result = self.config._derive_view(View,
+ renderer=renderer(), attr='index')
+ self.assertFalse(result is View)
+ self.assertEqual(result.__module__, View.__module__)
+ self.assertEqual(result.__doc__, View.__doc__)
+ self.assertEqual(result.__name__, View.__name__)
+ request = self._makeRequest()
+ context = testing.DummyResource()
+ self.assertEqual(result(context, request), response)
+
+ def test_as_newstyle_class_requestonly_attr_and_renderer(self):
+ response = DummyResponse()
+ class renderer(object):
+ def render_view(inself, req, resp, view_inst, ctx):
+ self.assertEqual(req, request)
+ self.assertEqual(resp, {'a':'1'})
+ self.assertEqual(view_inst.__class__, View)
+ self.assertEqual(ctx, context)
+ return response
+ def clone(self):
+ return self
+ class View(object):
+ def __init__(self, request):
+ pass
+ def index(self):
+ return {'a':'1'}
+ result = self.config.derive_view(View,
+ renderer=renderer(), attr='index')
+ self.assertFalse(result is View)
+ self.assertEqual(result.__module__, View.__module__)
+ self.assertEqual(result.__doc__, View.__doc__)
+ self.assertEqual(result.__name__, View.__name__)
+ request = self._makeRequest()
+ context = testing.DummyResource()
+ self.assertEqual(result(context, request), response)
+
+ def test_as_oldstyle_cls_context_request_attr_and_renderer(self):
+ response = DummyResponse()
+ class renderer(object):
+ def render_view(inself, req, resp, view_inst, ctx):
+ self.assertEqual(req, request)
+ self.assertEqual(resp, {'a':'1'})
+ self.assertEqual(view_inst.__class__, View)
+ self.assertEqual(ctx, context)
+ return response
+ def clone(self):
+ return self
+ class View:
+ def __init__(self, context, request):
+ pass
+ def index(self):
+ return {'a':'1'}
+ result = self.config.derive_view(View,
+ renderer=renderer(), attr='index')
+ self.assertFalse(result is View)
+ self.assertEqual(result.__module__, View.__module__)
+ self.assertEqual(result.__doc__, View.__doc__)
+ self.assertEqual(result.__name__, View.__name__)
+ request = self._makeRequest()
+ context = testing.DummyResource()
+ self.assertEqual(result(context, request), response)
+
+ def test_as_oldstyle_cls_requestonly_attr_and_renderer(self):
+ response = DummyResponse()
+ class renderer(object):
+ def render_view(inself, req, resp, view_inst, ctx):
+ self.assertEqual(req, request)
+ self.assertEqual(resp, {'a':'1'})
+ self.assertEqual(view_inst.__class__, View)
+ self.assertEqual(ctx, context)
+ return response
+ def clone(self):
+ return self
+ class View:
+ def __init__(self, request):
+ pass
+ def index(self):
+ return {'a':'1'}
+ result = self.config.derive_view(View,
+ renderer=renderer(), attr='index')
+ self.assertFalse(result is View)
+ self.assertEqual(result.__module__, View.__module__)
+ self.assertEqual(result.__doc__, View.__doc__)
+ self.assertEqual(result.__name__, View.__name__)
+ request = self._makeRequest()
+ context = testing.DummyResource()
+ self.assertEqual(result(context, request), response)
+
+ def test_as_instance_context_and_request_attr_and_renderer(self):
+ response = DummyResponse()
+ class renderer(object):
+ def render_view(inself, req, resp, view_inst, ctx):
+ self.assertEqual(req, request)
+ self.assertEqual(resp, {'a':'1'})
+ self.assertEqual(view_inst, view)
+ self.assertEqual(ctx, context)
+ return response
+ def clone(self):
+ return self
+ class View:
+ def index(self, context, request):
+ return {'a':'1'}
+ view = View()
+ result = self.config.derive_view(view,
+ renderer=renderer(), attr='index')
+ self.assertFalse(result is view)
+ self.assertEqual(result.__module__, view.__module__)
+ self.assertEqual(result.__doc__, view.__doc__)
+ request = self._makeRequest()
+ context = testing.DummyResource()
+ self.assertEqual(result(context, request), response)
+
+ def test_as_instance_requestonly_attr_and_renderer(self):
+ response = DummyResponse()
+ class renderer(object):
+ def render_view(inself, req, resp, view_inst, ctx):
+ self.assertEqual(req, request)
+ self.assertEqual(resp, {'a':'1'})
+ self.assertEqual(view_inst, view)
+ self.assertEqual(ctx, context)
+ return response
+ def clone(self):
+ return self
+ class View:
+ def index(self, request):
+ return {'a':'1'}
+ view = View()
+ result = self.config.derive_view(view,
+ renderer=renderer(), attr='index')
+ self.assertFalse(result is view)
+ self.assertEqual(result.__module__, view.__module__)
+ self.assertEqual(result.__doc__, view.__doc__)
+ request = self._makeRequest()
+ context = testing.DummyResource()
+ self.assertEqual(result(context, request), response)
+
+ def test_with_view_mapper_config_specified(self):
+ response = DummyResponse()
+ class mapper(object):
+ def __init__(self, **kw):
+ self.kw = kw
+ def __call__(self, view):
+ def wrapped(context, request):
+ return response
+ return wrapped
+ def view(context, request): return 'NOTOK'
+ result = self.config._derive_view(view, mapper=mapper)
+ self.assertFalse(result.__wraps__ is view)
+ self.assertEqual(result(None, None), response)
+
+ def test_with_view_mapper_view_specified(self):
+ from pyramid.response import Response
+ response = Response()
+ def mapper(**kw):
+ def inner(view):
+ def superinner(context, request):
+ self.assertEqual(request, None)
+ return response
+ return superinner
+ return inner
+ def view(context, request): return 'NOTOK'
+ view.__view_mapper__ = mapper
+ result = self.config.derive_view(view)
+ self.assertFalse(result.__wraps__ is view)
+ self.assertEqual(result(None, None), response)
+
+ def test_with_view_mapper_default_mapper_specified(self):
+ from pyramid.response import Response
+ response = Response()
+ def mapper(**kw):
+ def inner(view):
+ def superinner(context, request):
+ self.assertEqual(request, None)
+ return response
+ return superinner
+ return inner
+ self.config.set_view_mapper(mapper)
+ def view(context, request): return 'NOTOK'
+ result = self.config.derive_view(view)
+ self.assertFalse(result.__wraps__ is view)
+ self.assertEqual(result(None, None), response)
+
+ def test_attr_wrapped_view_branching_default_phash(self):
+ from pyramid.config.util import DEFAULT_PHASH
+ def view(context, request): pass
+ result = self.config._derive_view(view, phash=DEFAULT_PHASH)
+ self.assertEqual(result.__wraps__, view)
+
+ def test_attr_wrapped_view_branching_nondefault_phash(self):
+ def view(context, request): pass
+ result = self.config._derive_view(view, phash='nondefault')
+ self.assertNotEqual(result, view)
+
+ def test_http_cached_view_integer(self):
+ import datetime
+ from pyramid.response import Response
+ response = Response('OK')
+ def inner_view(context, request):
+ return response
+ result = self.config._derive_view(inner_view, http_cache=3600)
+ self.assertFalse(result is inner_view)
+ self.assertEqual(inner_view.__module__, result.__module__)
+ self.assertEqual(inner_view.__doc__, result.__doc__)
+ request = self._makeRequest()
+ when = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
+ result = result(None, request)
+ self.assertEqual(result, response)
+ headers = dict(result.headerlist)
+ expires = parse_httpdate(headers['Expires'])
+ assert_similar_datetime(expires, when)
+ self.assertEqual(headers['Cache-Control'], 'max-age=3600')
+
+ def test_http_cached_view_timedelta(self):
+ import datetime
+ from pyramid.response import Response
+ response = Response('OK')
+ def inner_view(context, request):
+ return response
+ result = self.config._derive_view(inner_view,
+ http_cache=datetime.timedelta(hours=1))
+ self.assertFalse(result is inner_view)
+ self.assertEqual(inner_view.__module__, result.__module__)
+ self.assertEqual(inner_view.__doc__, result.__doc__)
+ request = self._makeRequest()
+ when = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
+ result = result(None, request)
+ self.assertEqual(result, response)
+ headers = dict(result.headerlist)
+ expires = parse_httpdate(headers['Expires'])
+ assert_similar_datetime(expires, when)
+ self.assertEqual(headers['Cache-Control'], 'max-age=3600')
+
+ def test_http_cached_view_tuple(self):
+ import datetime
+ from pyramid.response import Response
+ response = Response('OK')
+ def inner_view(context, request):
+ return response
+ result = self.config._derive_view(inner_view,
+ http_cache=(3600, {'public':True}))
+ self.assertFalse(result is inner_view)
+ self.assertEqual(inner_view.__module__, result.__module__)
+ self.assertEqual(inner_view.__doc__, result.__doc__)
+ request = self._makeRequest()
+ when = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
+ result = result(None, request)
+ self.assertEqual(result, response)
+ headers = dict(result.headerlist)
+ expires = parse_httpdate(headers['Expires'])
+ assert_similar_datetime(expires, when)
+ self.assertEqual(headers['Cache-Control'], 'max-age=3600, public')
+
+ def test_http_cached_view_tuple_seconds_None(self):
+ from pyramid.response import Response
+ response = Response('OK')
+ def inner_view(context, request):
+ return response
+ result = self.config._derive_view(inner_view,
+ http_cache=(None, {'public':True}))
+ self.assertFalse(result is inner_view)
+ self.assertEqual(inner_view.__module__, result.__module__)
+ self.assertEqual(inner_view.__doc__, result.__doc__)
+ request = self._makeRequest()
+ result = result(None, request)
+ self.assertEqual(result, response)
+ headers = dict(result.headerlist)
+ self.assertFalse('Expires' in headers)
+ self.assertEqual(headers['Cache-Control'], 'public')
+
+ def test_http_cached_view_prevent_auto_set(self):
+ from pyramid.response import Response
+ response = Response()
+ response.cache_control.prevent_auto = True
+ def inner_view(context, request):
+ return response
+ result = self.config._derive_view(inner_view, http_cache=3600)
+ request = self._makeRequest()
+ result = result(None, request)
+ self.assertEqual(result, response) # doesn't blow up
+ headers = dict(result.headerlist)
+ self.assertFalse('Expires' in headers)
+ self.assertFalse('Cache-Control' in headers)
+
+ def test_http_cached_prevent_http_cache_in_settings(self):
+ self.config.registry.settings['prevent_http_cache'] = True
+ from pyramid.response import Response
+ response = Response()
+ def inner_view(context, request):
+ return response
+ result = self.config._derive_view(inner_view, http_cache=3600)
+ request = self._makeRequest()
+ result = result(None, request)
+ self.assertEqual(result, response)
+ headers = dict(result.headerlist)
+ self.assertFalse('Expires' in headers)
+ self.assertFalse('Cache-Control' in headers)
+
+ def test_http_cached_view_bad_tuple(self):
+ def view(request): pass
+ self.assertRaises(ConfigurationError, self.config._derive_view,
+ view, http_cache=(None,))
+
+ def test_csrf_view_requires_bool_or_str_in_require_csrf(self):
+ def view(request): pass
+ try:
+ self.config._derive_view(view, require_csrf=object())
+ except ConfigurationError as ex:
+ self.assertEqual(
+ 'View option "require_csrf" must be a string or boolean value',
+ ex.args[0])
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_csrf_view_requires_bool_or_str_in_config_setting(self):
+ def view(request): pass
+ self.config.add_settings({'pyramid.require_default_csrf': object()})
+ try:
+ self.config._derive_view(view)
+ except ConfigurationError as ex:
+ self.assertEqual(
+ 'Config setting "pyramid.require_csrf_default" must be a '
+ 'string or boolean value',
+ ex.args[0])
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_csrf_view_requires_header(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.POST = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.headers = {'X-CSRF-Token': 'foo'}
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_requires_param(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'DUMMY': 'foo'}
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_https_domain(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.scheme = "https"
+ request.domain = "example.com"
+ request.host_port = 443
+ request.referrer = "https://example.com/login/"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'DUMMY': 'foo'}
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_ignores_GET(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'GET'
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_fails_on_bad_POST_param(self):
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'DUMMY': 'bar'}
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
+ def test_csrf_view_fails_on_bad_POST_header(self):
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.POST = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.headers = {'X-CSRF-Token': 'bar'}
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
+ def test_csrf_view_fails_on_bad_PUT_header(self):
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'PUT'
+ request.POST = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.headers = {'X-CSRF-Token': 'bar'}
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
+ def test_csrf_view_fails_on_bad_referrer(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.method = "POST"
+ request.scheme = "https"
+ request.host_port = 443
+ request.domain = "example.com"
+ request.referrer = "https://not-example.com/evil/"
+ request.registry.settings = {}
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ self.assertRaises(BadCSRFOrigin, lambda: view(None, request))
+
+ def test_csrf_view_fails_on_bad_origin(self):
+ from pyramid.exceptions import BadCSRFOrigin
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.method = "POST"
+ request.scheme = "https"
+ request.host_port = 443
+ request.domain = "example.com"
+ request.headers = {"Origin": "https://not-example.com/evil/"}
+ request.registry.settings = {}
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ self.assertRaises(BadCSRFOrigin, lambda: view(None, request))
+
+ def test_csrf_view_uses_config_setting_truthy(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'csrf_token': 'foo'}
+ self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
+ view = self.config._derive_view(inner_view)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_uses_config_setting_with_custom_token(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'DUMMY': 'foo'}
+ self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'})
+ view = self.config._derive_view(inner_view)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_uses_config_setting_falsey(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.params['csrf_token'] = 'foo'
+ self.config.add_settings({'pyramid.require_default_csrf': 'no'})
+ view = self.config._derive_view(inner_view)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_uses_view_option_override(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'DUMMY': 'foo'}
+ self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
+ view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+ def test_csrf_view_uses_config_setting_when_view_option_is_true(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'DUMMY': 'foo'}
+ self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'})
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
+
+class TestDerivationOrder(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ self.config = None
+ testing.tearDown()
+
+ def test_right_order_user_sorted(self):
+ from pyramid.interfaces import IViewDerivers
+
+ self.config.add_view_deriver(None, 'deriv1')
+ self.config.add_view_deriver(None, 'deriv2', 'decorated_view', 'deriv1')
+ self.config.add_view_deriver(None, 'deriv3', 'deriv2', 'deriv1')
+
+ derivers = self.config.registry.getUtility(IViewDerivers)
+ derivers_sorted = derivers.sorted()
+ dlist = [d for (d, _) in derivers_sorted]
+ self.assertEqual([
+ 'secured_view',
+ 'csrf_view',
+ 'owrapped_view',
+ 'http_cached_view',
+ 'decorated_view',
+ 'deriv2',
+ 'deriv3',
+ 'deriv1',
+ 'rendered_view',
+ 'mapped_view',
+ ], dlist)
+
+ def test_right_order_implicit(self):
+ from pyramid.interfaces import IViewDerivers
+
+ self.config.add_view_deriver(None, 'deriv1')
+ self.config.add_view_deriver(None, 'deriv2')
+ self.config.add_view_deriver(None, 'deriv3')
+
+ derivers = self.config.registry.getUtility(IViewDerivers)
+ derivers_sorted = derivers.sorted()
+ dlist = [d for (d, _) in derivers_sorted]
+ self.assertEqual([
+ 'secured_view',
+ 'csrf_view',
+ 'owrapped_view',
+ 'http_cached_view',
+ 'decorated_view',
+ 'deriv3',
+ 'deriv2',
+ 'deriv1',
+ 'rendered_view',
+ 'mapped_view',
+ ], dlist)
+
+ def test_right_order_under_rendered_view(self):
+ from pyramid.interfaces import IViewDerivers
+
+ self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view')
+
+ derivers = self.config.registry.getUtility(IViewDerivers)
+ derivers_sorted = derivers.sorted()
+ dlist = [d for (d, _) in derivers_sorted]
+ self.assertEqual([
+ 'secured_view',
+ 'csrf_view',
+ 'owrapped_view',
+ 'http_cached_view',
+ 'decorated_view',
+ 'rendered_view',
+ 'deriv1',
+ 'mapped_view',
+ ], dlist)
+
+
+ def test_right_order_under_rendered_view_others(self):
+ from pyramid.interfaces import IViewDerivers
+
+ self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view')
+ self.config.add_view_deriver(None, 'deriv2')
+ self.config.add_view_deriver(None, 'deriv3')
+
+ derivers = self.config.registry.getUtility(IViewDerivers)
+ derivers_sorted = derivers.sorted()
+ dlist = [d for (d, _) in derivers_sorted]
+ self.assertEqual([
+ 'secured_view',
+ 'csrf_view',
+ 'owrapped_view',
+ 'http_cached_view',
+ 'decorated_view',
+ 'deriv3',
+ 'deriv2',
+ 'rendered_view',
+ 'deriv1',
+ 'mapped_view',
+ ], dlist)
+
+
+class TestAddDeriver(unittest.TestCase):
+
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ self.config = None
+ testing.tearDown()
+
+ def test_add_single_deriver(self):
+ response = DummyResponse()
+ response.deriv = False
+ view = lambda *arg: response
+
+ def deriv(view, info):
+ self.assertFalse(response.deriv)
+ response.deriv = True
+ return view
+
+ result = self.config._derive_view(view)
+ self.assertFalse(response.deriv)
+ self.config.add_view_deriver(deriv, 'test_deriv')
+
+ result = self.config._derive_view(view)
+ self.assertTrue(response.deriv)
+
+ def test_override_deriver(self):
+ flags = {}
+
+ class AView:
+ def __init__(self):
+ self.response = DummyResponse()
+
+ def deriv1(view, info):
+ flags['deriv1'] = True
+ return view
+
+ def deriv2(view, info):
+ flags['deriv2'] = True
+ return view
+
+ view1 = AView()
+ self.config.add_view_deriver(deriv1, 'test_deriv')
+ result = self.config._derive_view(view1)
+ self.assertTrue(flags.get('deriv1'))
+ self.assertFalse(flags.get('deriv2'))
+
+ flags.clear()
+ view2 = AView()
+ self.config.add_view_deriver(deriv2, 'test_deriv')
+ result = self.config._derive_view(view2)
+ self.assertFalse(flags.get('deriv1'))
+ self.assertTrue(flags.get('deriv2'))
+
+ def test_override_mapped_view(self):
+ from pyramid.viewderivers import VIEW
+ response = DummyResponse()
+ view = lambda *arg: response
+ flags = {}
+
+ def deriv1(view, info):
+ flags['deriv1'] = True
+ return view
+
+ result = self.config._derive_view(view)
+ self.assertFalse(flags.get('deriv1'))
+
+ flags.clear()
+ self.config.add_view_deriver(
+ deriv1, name='mapped_view', under='rendered_view', over=VIEW)
+ result = self.config._derive_view(view)
+ self.assertTrue(flags.get('deriv1'))
+
+ def test_add_multi_derivers_ordered(self):
+ from pyramid.viewderivers import INGRESS
+ response = DummyResponse()
+ view = lambda *arg: response
+ response.deriv = []
+
+ def deriv1(view, info):
+ response.deriv.append('deriv1')
+ return view
+
+ def deriv2(view, info):
+ response.deriv.append('deriv2')
+ return view
+
+ def deriv3(view, info):
+ response.deriv.append('deriv3')
+ return view
+
+ self.config.add_view_deriver(deriv1, 'deriv1')
+ self.config.add_view_deriver(deriv2, 'deriv2', INGRESS, 'deriv1')
+ self.config.add_view_deriver(deriv3, 'deriv3', 'deriv2', 'deriv1')
+ result = self.config._derive_view(view)
+ self.assertEqual(response.deriv, ['deriv1', 'deriv3', 'deriv2'])
+
+ def test_add_deriver_without_name(self):
+ from pyramid.interfaces import IViewDerivers
+ def deriv1(view, info): pass
+ self.config.add_view_deriver(deriv1)
+ derivers = self.config.registry.getUtility(IViewDerivers)
+ self.assertTrue('deriv1' in derivers.names)
+
+ def test_add_deriver_reserves_ingress(self):
+ from pyramid.exceptions import ConfigurationError
+ from pyramid.viewderivers import INGRESS
+ def deriv1(view, info): pass
+ self.assertRaises(
+ ConfigurationError, self.config.add_view_deriver, deriv1, INGRESS)
+
+ def test_add_deriver_enforces_ingress_is_first(self):
+ from pyramid.exceptions import ConfigurationError
+ from pyramid.viewderivers import INGRESS
+ def deriv1(view, info): pass
+ try:
+ self.config.add_view_deriver(deriv1, over=INGRESS)
+ except ConfigurationError as ex:
+ self.assertTrue('cannot be over INGRESS' in ex.args[0])
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_add_deriver_enforces_view_is_last(self):
+ from pyramid.exceptions import ConfigurationError
+ from pyramid.viewderivers import VIEW
+ def deriv1(view, info): pass
+ try:
+ self.config.add_view_deriver(deriv1, under=VIEW)
+ except ConfigurationError as ex:
+ self.assertTrue('cannot be under VIEW' in ex.args[0])
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_add_deriver_enforces_mapped_view_is_last(self):
+ from pyramid.exceptions import ConfigurationError
+ def deriv1(view, info): pass
+ try:
+ self.config.add_view_deriver(deriv1, 'deriv1', under='mapped_view')
+ except ConfigurationError as ex:
+ self.assertTrue('cannot be under "mapped_view"' in ex.args[0])
+ else: # pragma: no cover
+ raise AssertionError
+
+
+class TestDeriverIntegration(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ self.config = None
+ testing.tearDown()
+
+ def _getViewCallable(self, config, ctx_iface=None, request_iface=None,
+ name=''):
+ from zope.interface import Interface
+ from pyramid.interfaces import IRequest
+ from pyramid.interfaces import IView
+ from pyramid.interfaces import IViewClassifier
+ from pyramid.interfaces import IExceptionViewClassifier
+ classifier = IViewClassifier
+ if ctx_iface is None:
+ ctx_iface = Interface
+ if request_iface is None:
+ request_iface = IRequest
+ return config.registry.adapters.lookup(
+ (classifier, request_iface, ctx_iface), IView, name=name,
+ default=None)
+
+ def _makeRequest(self, config):
+ request = DummyRequest()
+ request.registry = config.registry
+ return request
+
+ def test_view_options(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ response.deriv = []
+
+ def deriv1(view, info):
+ response.deriv.append(info.options['deriv1'])
+ return view
+ deriv1.options = ('deriv1',)
+
+ def deriv2(view, info):
+ response.deriv.append(info.options['deriv2'])
+ return view
+ deriv2.options = ('deriv2',)
+
+ self.config.add_view_deriver(deriv1, 'deriv1')
+ self.config.add_view_deriver(deriv2, 'deriv2')
+ self.config.add_view(view, deriv1='test1', deriv2='test2')
+
+ wrapper = self._getViewCallable(self.config)
+ request = self._makeRequest(self.config)
+ request.method = 'GET'
+ self.assertEqual(wrapper(None, request), response)
+ self.assertEqual(['test1', 'test2'], response.deriv)
+
+ def test_unexpected_view_options(self):
+ from pyramid.exceptions import ConfigurationError
+ def deriv1(view, info): pass
+ self.config.add_view_deriver(deriv1, 'deriv1')
+ self.assertRaises(
+ ConfigurationError,
+ lambda: self.config.add_view(lambda r: {}, deriv1='test1'))
+
+@implementer(IResponse)
+class DummyResponse(object):
+ content_type = None
+ default_content_type = None
+ body = None
+
+class DummyRequest:
+ subpath = ()
+ matchdict = None
+ request_iface = IRequest
+
+ def __init__(self, environ=None):
+ if environ is None:
+ environ = {}
+ self.environ = environ
+ self.params = {}
+ self.cookies = {}
+ self.headers = {}
+ self.response = DummyResponse()
+
+class DummyLogger:
+ def __init__(self):
+ self.messages = []
+ def info(self, msg):
+ self.messages.append(msg)
+ warn = info
+ debug = info
+
+class DummySecurityPolicy:
+ def __init__(self, permitted=True):
+ self.permitted = permitted
+
+ def effective_principals(self, request):
+ return []
+
+ def permits(self, context, principals, permission):
+ return self.permitted
+
+class DummySession(dict):
+ def get_csrf_token(self):
+ return self['csrf_token']
+
+def parse_httpdate(s):
+ import datetime
+ # cannot use %Z, must use literal GMT; Jython honors timezone
+ # but CPython does not
+ return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S GMT")
+
+def assert_similar_datetime(one, two):
+ for attr in ('year', 'month', 'day', 'hour', 'minute'):
+ one_attr = getattr(one, attr)
+ two_attr = getattr(two, attr)
+ if not one_attr == two_attr: # pragma: no cover
+ raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr))
diff --git a/pyramid/util.py b/pyramid/util.py
index 0a73cedaf..4936dcb24 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -1,3 +1,4 @@
+import contextlib
import functools
try:
# py2.7.7+ and py3.3+ have native comparison support
@@ -380,6 +381,9 @@ class TopologicalSorter(object):
self.first = first
self.last = last
+ def values(self):
+ return self.name2val.values()
+
def remove(self, name):
""" Remove a node from the sort input """
self.names.remove(name)
@@ -591,3 +595,39 @@ def get_callable_name(name):
'used on __name__ of the method'
)
raise ConfigurationError(msg % name)
+
+@contextlib.contextmanager
+def hide_attrs(obj, *attrs):
+ """
+ Temporarily delete object attrs and restore afterward.
+ """
+ obj_vals = obj.__dict__ if obj is not None else {}
+ saved_vals = {}
+ for name in attrs:
+ saved_vals[name] = obj_vals.pop(name, _marker)
+ try:
+ yield
+ finally:
+ for name in attrs:
+ saved_val = saved_vals[name]
+ if saved_val is not _marker:
+ obj_vals[name] = saved_val
+ elif name in obj_vals:
+ del obj_vals[name]
+
+
+def is_same_domain(host, pattern):
+ """
+ Return ``True`` if the host is either an exact match or a match
+ to the wildcard pattern.
+ Any pattern beginning with a period matches a domain and all of its
+ subdomains. (e.g. ``.example.com`` matches ``example.com`` and
+ ``foo.example.com``). Anything else is an exact string match.
+ """
+ if not pattern:
+ return False
+
+ pattern = pattern.lower()
+ return (pattern[0] == "." and
+ (host.endswith(pattern) or host == pattern[1:]) or
+ pattern == host)
diff --git a/pyramid/view.py b/pyramid/view.py
index 7e8996ca4..62ac5310e 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -1,4 +1,6 @@
import itertools
+import sys
+
import venusian
from zope.interface import providedBy
@@ -10,6 +12,7 @@ from pyramid.interfaces import (
IView,
IViewClassifier,
IRequest,
+ IExceptionViewClassifier,
)
from pyramid.compat import decode_path_info
@@ -22,6 +25,7 @@ from pyramid.httpexceptions import (
)
from pyramid.threadlocal import get_current_registry
+from pyramid.util import hide_attrs
_marker = object()
@@ -165,7 +169,8 @@ class view_config(object):
``request_type``, ``route_name``, ``request_method``, ``request_param``,
``containment``, ``xhr``, ``accept``, ``header``, ``path_info``,
``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``,
- ``match_param``, ``check_csrf``, ``physical_path``, and ``predicates``.
+ ``require_csrf``, ``match_param``, ``check_csrf``, ``physical_path``, and
+ ``view_options``.
The meanings of these arguments are the same as the arguments passed to
:meth:`pyramid.config.Configurator.add_view`. If any argument is left
@@ -547,3 +552,75 @@ def _call_view(
raise pme
return response
+
+class ViewMethodsMixin(object):
+ """ Request methods mixin for BaseRequest having to do with executing
+ views """
+ def invoke_exception_view(
+ self,
+ exc_info=None,
+ request=None,
+ secure=True
+ ):
+ """ Executes an exception view related to the request it's called upon.
+ The arguments it takes are these:
+
+ ``exc_info``
+
+ If provided, should be a 3-tuple in the form provided by
+ ``sys.exc_info()``. If not provided,
+ ``sys.exc_info()`` will be called to obtain the current
+ interpreter exception information. Default: ``None``.
+
+ ``request``
+
+ If the request to be used is not the same one as the instance that
+ this method is called upon, it may be passed here. Default:
+ ``None``.
+
+ ``secure``
+
+ If the exception view should not be rendered if the current user
+ does not have the appropriate permission, this should be ``True``.
+ Default: ``True``.
+
+ If called with no arguments, it uses the global exception information
+ returned by ``sys.exc_info()`` as ``exc_info``, the request
+ object that this method is attached to as the ``request``, and
+ ``True`` for ``secure``.
+
+ This method returns a :term:`response` object or ``None`` if no
+ matching exception view can be found."""
+
+ if request is None:
+ request = self
+ registry = getattr(request, 'registry', None)
+ if registry is None:
+ registry = get_current_registry()
+ if exc_info is None:
+ exc_info = sys.exc_info()
+ exc = exc_info[1]
+ attrs = request.__dict__
+ context_iface = providedBy(exc)
+
+ # clear old generated request.response, if any; it may
+ # have been mutated by the view, and its state is not
+ # sane (e.g. caching headers)
+ with hide_attrs(request, 'exception', 'exc_info', 'response'):
+ attrs['exception'] = exc
+ attrs['exc_info'] = exc_info
+ # we use .get instead of .__getitem__ below due to
+ # https://github.com/Pylons/pyramid/issues/700
+ request_iface = attrs.get('request_iface', IRequest)
+ response = _call_view(
+ registry,
+ request,
+ exc,
+ context_iface,
+ '',
+ view_types=None,
+ view_classifier=IExceptionViewClassifier,
+ secure=secure,
+ request_iface=request_iface.combined,
+ )
+ return response
diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py
new file mode 100644
index 000000000..d5a5c480a
--- /dev/null
+++ b/pyramid/viewderivers.py
@@ -0,0 +1,508 @@
+import inspect
+
+from zope.interface import (
+ implementer,
+ provider,
+ )
+
+from pyramid.security import NO_PERMISSION_REQUIRED
+from pyramid.session import (
+ check_csrf_origin,
+ check_csrf_token,
+)
+from pyramid.response import Response
+
+from pyramid.interfaces import (
+ IAuthenticationPolicy,
+ IAuthorizationPolicy,
+ IDebugLogger,
+ IResponse,
+ IViewMapper,
+ IViewMapperFactory,
+ )
+
+from pyramid.compat import (
+ string_types,
+ is_bound_method,
+ is_unbound_method,
+ )
+
+from pyramid.config.util import (
+ DEFAULT_PHASH,
+ MAX_ORDER,
+ takes_one_arg,
+ )
+
+from pyramid.exceptions import (
+ ConfigurationError,
+ PredicateMismatch,
+ )
+from pyramid.httpexceptions import HTTPForbidden
+from pyramid.settings import (
+ falsey,
+ truthy,
+)
+from pyramid.util import object_description
+from pyramid.view import render_view_to_response
+from pyramid import renderers
+
+
+def view_description(view):
+ try:
+ return view.__text__
+ except AttributeError:
+ # custom view mappers might not add __text__
+ return object_description(view)
+
+def requestonly(view, attr=None):
+ return takes_one_arg(view, attr=attr, argname='request')
+
+@implementer(IViewMapper)
+@provider(IViewMapperFactory)
+class DefaultViewMapper(object):
+ def __init__(self, **kw):
+ self.attr = kw.get('attr')
+
+ def __call__(self, view):
+ if is_unbound_method(view) and self.attr is None:
+ raise ConfigurationError((
+ 'Unbound method calls are not supported, please set the class '
+ 'as your `view` and the method as your `attr`'
+ ))
+
+ if inspect.isclass(view):
+ view = self.map_class(view)
+ else:
+ view = self.map_nonclass(view)
+ return view
+
+ def map_class(self, view):
+ ronly = requestonly(view, self.attr)
+ if ronly:
+ mapped_view = self.map_class_requestonly(view)
+ else:
+ mapped_view = self.map_class_native(view)
+ mapped_view.__text__ = 'method %s of %s' % (
+ self.attr or '__call__', object_description(view))
+ return mapped_view
+
+ def map_nonclass(self, view):
+ # We do more work here than appears necessary to avoid wrapping the
+ # view unless it actually requires wrapping (to avoid function call
+ # overhead).
+ mapped_view = view
+ ronly = requestonly(view, self.attr)
+ if ronly:
+ mapped_view = self.map_nonclass_requestonly(view)
+ elif self.attr:
+ mapped_view = self.map_nonclass_attr(view)
+ if inspect.isroutine(mapped_view):
+ # This branch will be true if the view is a function or a method.
+ # We potentially mutate an unwrapped object here if it's a
+ # function. We do this to avoid function call overhead of
+ # injecting another wrapper. However, we must wrap if the
+ # function is a bound method because we can't set attributes on a
+ # bound method.
+ if is_bound_method(view):
+ _mapped_view = mapped_view
+ def mapped_view(context, request):
+ return _mapped_view(context, request)
+ if self.attr is not None:
+ mapped_view.__text__ = 'attr %s of %s' % (
+ self.attr, object_description(view))
+ else:
+ mapped_view.__text__ = object_description(view)
+ return mapped_view
+
+ def map_class_requestonly(self, view):
+ # its a class that has an __init__ which only accepts request
+ attr = self.attr
+ def _class_requestonly_view(context, request):
+ inst = view(request)
+ request.__view__ = inst
+ if attr is None:
+ response = inst()
+ else:
+ response = getattr(inst, attr)()
+ return response
+ return _class_requestonly_view
+
+ def map_class_native(self, view):
+ # its a class that has an __init__ which accepts both context and
+ # request
+ attr = self.attr
+ def _class_view(context, request):
+ inst = view(context, request)
+ request.__view__ = inst
+ if attr is None:
+ response = inst()
+ else:
+ response = getattr(inst, attr)()
+ return response
+ return _class_view
+
+ def map_nonclass_requestonly(self, view):
+ # its a function that has a __call__ which accepts only a single
+ # request argument
+ attr = self.attr
+ def _requestonly_view(context, request):
+ if attr is None:
+ response = view(request)
+ else:
+ response = getattr(view, attr)(request)
+ return response
+ return _requestonly_view
+
+ def map_nonclass_attr(self, view):
+ # its a function that has a __call__ which accepts both context and
+ # request, but still has an attr
+ def _attr_view(context, request):
+ response = getattr(view, self.attr)(context, request)
+ return response
+ return _attr_view
+
+
+def wraps_view(wrapper):
+ def inner(view, info):
+ wrapper_view = wrapper(view, info)
+ return preserve_view_attrs(view, wrapper_view)
+ return inner
+
+def preserve_view_attrs(view, wrapper):
+ if view is None:
+ return wrapper
+
+ if wrapper is view:
+ return view
+
+ original_view = getattr(view, '__original_view__', None)
+
+ if original_view is None:
+ original_view = view
+
+ wrapper.__wraps__ = view
+ wrapper.__original_view__ = original_view
+ wrapper.__module__ = view.__module__
+ wrapper.__doc__ = view.__doc__
+
+ try:
+ wrapper.__name__ = view.__name__
+ except AttributeError:
+ wrapper.__name__ = repr(view)
+
+ # attrs that may not exist on "view", but, if so, must be attached to
+ # "wrapped view"
+ for attr in ('__permitted__', '__call_permissive__', '__permission__',
+ '__predicated__', '__predicates__', '__accept__',
+ '__order__', '__text__'):
+ try:
+ setattr(wrapper, attr, getattr(view, attr))
+ except AttributeError:
+ pass
+
+ return wrapper
+
+def mapped_view(view, info):
+ mapper = info.options.get('mapper')
+ if mapper is None:
+ mapper = getattr(view, '__view_mapper__', None)
+ if mapper is None:
+ mapper = info.registry.queryUtility(IViewMapperFactory)
+ if mapper is None:
+ mapper = DefaultViewMapper
+
+ mapped_view = mapper(**info.options)(view)
+ return mapped_view
+
+mapped_view.options = ('mapper', 'attr')
+
+def owrapped_view(view, info):
+ wrapper_viewname = info.options.get('wrapper')
+ viewname = info.options.get('name')
+ if not wrapper_viewname:
+ return view
+ def _owrapped_view(context, request):
+ response = view(context, request)
+ request.wrapped_response = response
+ request.wrapped_body = response.body
+ request.wrapped_view = view
+ wrapped_response = render_view_to_response(context, request,
+ wrapper_viewname)
+ if wrapped_response is None:
+ raise ValueError(
+ 'No wrapper view named %r found when executing view '
+ 'named %r' % (wrapper_viewname, viewname))
+ return wrapped_response
+ return _owrapped_view
+
+owrapped_view.options = ('name', 'wrapper')
+
+def http_cached_view(view, info):
+ if info.settings.get('prevent_http_cache', False):
+ return view
+
+ seconds = info.options.get('http_cache')
+
+ if seconds is None:
+ return view
+
+ options = {}
+
+ if isinstance(seconds, (tuple, list)):
+ try:
+ seconds, options = seconds
+ except ValueError:
+ raise ConfigurationError(
+ 'If http_cache parameter is a tuple or list, it must be '
+ 'in the form (seconds, options); not %s' % (seconds,))
+
+ def wrapper(context, request):
+ response = view(context, request)
+ prevent_caching = getattr(response.cache_control, 'prevent_auto',
+ False)
+ if not prevent_caching:
+ response.cache_expires(seconds, **options)
+ return response
+
+ return wrapper
+
+http_cached_view.options = ('http_cache',)
+
+def secured_view(view, info):
+ for wrapper in (_secured_view, _authdebug_view):
+ view = wraps_view(wrapper)(view, info)
+ return view
+
+secured_view.options = ('permission',)
+
+def _secured_view(view, info):
+ permission = info.options.get('permission')
+ if permission == NO_PERMISSION_REQUIRED:
+ # allow views registered within configurations that have a
+ # default permission to explicitly override the default
+ # permission, replacing it with no permission at all
+ permission = None
+
+ wrapped_view = view
+ authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
+ authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+
+ if authn_policy and authz_policy and (permission is not None):
+ def _permitted(context, request):
+ principals = authn_policy.effective_principals(request)
+ return authz_policy.permits(context, principals, permission)
+ def _secured_view(context, request):
+ result = _permitted(context, request)
+ if result:
+ return view(context, request)
+ view_name = getattr(view, '__name__', view)
+ msg = getattr(
+ request, 'authdebug_message',
+ 'Unauthorized: %s failed permission check' % view_name)
+ raise HTTPForbidden(msg, result=result)
+ _secured_view.__call_permissive__ = view
+ _secured_view.__permitted__ = _permitted
+ _secured_view.__permission__ = permission
+ wrapped_view = _secured_view
+
+ return wrapped_view
+
+def _authdebug_view(view, info):
+ wrapped_view = view
+ settings = info.settings
+ permission = info.options.get('permission')
+ authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
+ authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+ logger = info.registry.queryUtility(IDebugLogger)
+ if settings and settings.get('debug_authorization', False):
+ def _authdebug_view(context, request):
+ view_name = getattr(request, 'view_name', None)
+
+ if authn_policy and authz_policy:
+ if permission is NO_PERMISSION_REQUIRED:
+ msg = 'Allowed (NO_PERMISSION_REQUIRED)'
+ elif permission is None:
+ msg = 'Allowed (no permission registered)'
+ else:
+ principals = authn_policy.effective_principals(request)
+ msg = str(authz_policy.permits(
+ context, principals, permission))
+ else:
+ msg = 'Allowed (no authorization policy in use)'
+
+ view_name = getattr(request, 'view_name', None)
+ url = getattr(request, 'url', None)
+ msg = ('debug_authorization of url %s (view name %r against '
+ 'context %r): %s' % (url, view_name, context, msg))
+ if logger:
+ logger.debug(msg)
+ if request is not None:
+ request.authdebug_message = msg
+ return view(context, request)
+
+ wrapped_view = _authdebug_view
+
+ return wrapped_view
+
+def predicated_view(view, info):
+ preds = info.predicates
+ if not preds:
+ return view
+ def predicate_wrapper(context, request):
+ for predicate in preds:
+ if not predicate(context, request):
+ view_name = getattr(view, '__name__', view)
+ raise PredicateMismatch(
+ 'predicate mismatch for view %s (%s)' % (
+ view_name, predicate.text()))
+ return view(context, request)
+ def checker(context, request):
+ return all((predicate(context, request) for predicate in
+ preds))
+ predicate_wrapper.__predicated__ = checker
+ predicate_wrapper.__predicates__ = preds
+ return predicate_wrapper
+
+def attr_wrapped_view(view, info):
+ accept, order, phash = (info.options.get('accept', None),
+ getattr(info, 'order', MAX_ORDER),
+ getattr(info, 'phash', DEFAULT_PHASH))
+ # this is a little silly but we don't want to decorate the original
+ # function with attributes that indicate accept, order, and phash,
+ # so we use a wrapper
+ if (
+ (accept is None) and
+ (order == MAX_ORDER) and
+ (phash == DEFAULT_PHASH)
+ ):
+ return view # defaults
+ def attr_view(context, request):
+ return view(context, request)
+ attr_view.__accept__ = accept
+ attr_view.__order__ = order
+ attr_view.__phash__ = phash
+ attr_view.__view_attr__ = info.options.get('attr')
+ attr_view.__permission__ = info.options.get('permission')
+ return attr_view
+
+attr_wrapped_view.options = ('accept', 'attr', 'permission')
+
+def rendered_view(view, info):
+ # one way or another this wrapper must produce a Response (unless
+ # the renderer is a NullRendererHelper)
+ renderer = info.options.get('renderer')
+ if renderer is None:
+ # register a default renderer if you want super-dynamic
+ # rendering. registering a default renderer will also allow
+ # override_renderer to work if a renderer is left unspecified for
+ # a view registration.
+ def viewresult_to_response(context, request):
+ result = view(context, request)
+ if result.__class__ is Response: # common case
+ response = result
+ else:
+ response = info.registry.queryAdapterOrSelf(result, IResponse)
+ if response is None:
+ if result is None:
+ append = (' You may have forgotten to return a value '
+ 'from the view callable.')
+ elif isinstance(result, dict):
+ append = (' You may have forgotten to define a '
+ 'renderer in the view configuration.')
+ else:
+ append = ''
+
+ msg = ('Could not convert return value of the view '
+ 'callable %s into a response object. '
+ 'The value returned was %r.' + append)
+
+ raise ValueError(msg % (view_description(view), result))
+
+ return response
+
+ return viewresult_to_response
+
+ if renderer is renderers.null_renderer:
+ return view
+
+ def rendered_view(context, request):
+ result = view(context, request)
+ if result.__class__ is Response: # potential common case
+ response = result
+ else:
+ # this must adapt, it can't do a simple interface check
+ # (avoid trying to render webob responses)
+ response = info.registry.queryAdapterOrSelf(result, IResponse)
+ if response is None:
+ attrs = getattr(request, '__dict__', {})
+ if 'override_renderer' in attrs:
+ # renderer overridden by newrequest event or other
+ renderer_name = attrs.pop('override_renderer')
+ view_renderer = renderers.RendererHelper(
+ name=renderer_name,
+ package=info.package,
+ registry=info.registry)
+ else:
+ view_renderer = renderer.clone()
+ if '__view__' in attrs:
+ view_inst = attrs.pop('__view__')
+ else:
+ view_inst = getattr(view, '__original_view__', view)
+ response = view_renderer.render_view(
+ request, result, view_inst, context)
+ return response
+
+ return rendered_view
+
+rendered_view.options = ('renderer',)
+
+def decorated_view(view, info):
+ decorator = info.options.get('decorator')
+ if decorator is None:
+ return view
+ return decorator(view)
+
+decorated_view.options = ('decorator',)
+
+def _parse_csrf_setting(val, error_source):
+ if val:
+ if isinstance(val, string_types):
+ if val.lower() in truthy:
+ val = True
+ elif val.lower() in falsey:
+ val = False
+ elif not isinstance(val, bool):
+ raise ConfigurationError(
+ '{0} must be a string or boolean value'
+ .format(error_source))
+ return val
+
+SAFE_REQUEST_METHODS = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
+
+def csrf_view(view, info):
+ default_val = _parse_csrf_setting(
+ info.settings.get('pyramid.require_default_csrf'),
+ 'Config setting "pyramid.require_csrf_default"')
+ val = _parse_csrf_setting(
+ info.options.get('require_csrf'),
+ 'View option "require_csrf"')
+ if (val is True and default_val) or val is None:
+ val = default_val
+ if val is True:
+ val = 'csrf_token'
+ wrapped_view = view
+ if val:
+ def csrf_view(context, request):
+ # Assume that anything not defined as 'safe' by RFC2616 needs
+ # protection
+ if request.method not in SAFE_REQUEST_METHODS:
+ check_csrf_origin(request, raises=True)
+ check_csrf_token(request, val, raises=True)
+ return view(context, request)
+ wrapped_view = csrf_view
+ return wrapped_view
+
+csrf_view.options = ('require_csrf',)
+
+VIEW = 'VIEW'
+INGRESS = 'INGRESS'
diff --git a/setup.py b/setup.py
index e878b9932..021da2b5f 100644
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,7 @@ try:
except IOError:
README = CHANGES = ''
-install_requires=[
+install_requires = [
'setuptools',
'WebOb >= 1.3.1', # request.domain and CookieProfile
'repoze.lru >= 0.4', # py3 compat
@@ -72,42 +72,41 @@ testing_extras = tests_require + [
]
setup(name='pyramid',
- version='1.7.dev0',
+ version='1.8.dev0',
description='The Pyramid Web Framework, a Pylons project',
- long_description=README + '\n\n' + CHANGES,
+ long_description=README + '\n\n' + CHANGES,
classifiers=[
- "Development Status :: 6 - Mature",
- "Intended Audience :: Developers",
- "Programming Language :: Python",
- "Programming Language :: Python :: 2.6",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.3",
- "Programming Language :: Python :: 3.4",
- "Programming Language :: Python :: 3.5",
- "Programming Language :: Python :: Implementation :: CPython",
- "Programming Language :: Python :: Implementation :: PyPy",
- "Framework :: Pyramid",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: WSGI",
- "License :: Repoze Public License",
- ],
+ "Development Status :: 6 - Mature",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI",
+ "License :: Repoze Public License",
+ ],
keywords='web wsgi pylons pyramid',
author="Chris McDonough, Agendaless Consulting",
author_email="pylons-discuss@googlegroups.com",
- url="http://docs.pylonsproject.org/en/latest/docs/pyramid.html",
+ url="https://trypyramid.com",
license="BSD-derived (http://www.repoze.org/LICENSE.txt)",
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- install_requires = install_requires,
- extras_require = {
- 'testing':testing_extras,
- 'docs':docs_extras,
+ install_requires=install_requires,
+ extras_require={
+ 'testing': testing_extras,
+ 'docs': docs_extras,
},
- tests_require = tests_require,
+ tests_require=tests_require,
test_suite="pyramid.tests",
- entry_points = """\
+ entry_points="""\
[pyramid.scaffold]
starter=pyramid.scaffolds:StarterProjectTemplate
zodb=pyramid.scaffolds:ZODBProjectTemplate
@@ -128,4 +127,3 @@ setup(name='pyramid',
cherrypy = pyramid.scripts.pserve:cherrypy_server_runner
"""
)
-
diff --git a/tox.ini b/tox.ini
index 096600aec..d29f41662 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
envlist =
- py26,py27,py33,py34,py35,pypy,pypy3,
+ py27,py33,py34,py35,pypy,pypy3,
docs,pep8,
{py2,py3}-cover,coverage,
@@ -8,7 +8,6 @@ envlist =
# Most of these are defaults but if you specify any you can't fall back
# to defaults for others.
basepython =
- py26: python2.6
py27: python2.7
py33: python3.3
py34: python3.4
@@ -22,14 +21,8 @@ commands =
pip install pyramid[testing]
nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:}
-[testenv:py26-scaffolds]
-basepython = python2.6
-commands =
- python pyramid/scaffolds/tests.py
-deps = virtualenv
-
[testenv:py27-scaffolds]
-basepython = python2.6
+basepython = python2.7
commands =
python pyramid/scaffolds/tests.py
deps = virtualenv