summaryrefslogtreecommitdiff
path: root/docs/tutorials
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tutorials')
-rw-r--r--docs/tutorials/modwsgi/index.rst139
-rw-r--r--docs/tutorials/wiki/NOTE-relocatable.txt7
-rw-r--r--docs/tutorials/wiki/authorization.rst436
-rw-r--r--docs/tutorials/wiki/background.rst8
-rw-r--r--docs/tutorials/wiki/basiclayout.rst109
-rw-r--r--docs/tutorials/wiki/definingmodels.rst36
-rw-r--r--docs/tutorials/wiki/definingviews.rst352
-rw-r--r--docs/tutorials/wiki/design.rst54
-rw-r--r--docs/tutorials/wiki/distributing.rst50
-rw-r--r--docs/tutorials/wiki/index.rst12
-rw-r--r--docs/tutorials/wiki/installation.rst416
-rw-r--r--docs/tutorials/wiki/src/authorization/.coveragerc3
-rw-r--r--docs/tutorials/wiki/src/authorization/.gitignore21
-rw-r--r--docs/tutorials/wiki/src/authorization/CHANGES.txt3
-rw-r--r--docs/tutorials/wiki/src/authorization/README.txt27
-rw-r--r--docs/tutorials/wiki/src/authorization/development.ini17
-rw-r--r--docs/tutorials/wiki/src/authorization/production.ini18
-rw-r--r--docs/tutorials/wiki/src/authorization/pytest.ini3
-rw-r--r--docs/tutorials/wiki/src/authorization/setup.py68
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/__init__.py19
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/models.py4
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/pshell.py11
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/security.py17
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/static/transparent.gifbin49 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt120
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt119
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt73
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt121
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/tests.py124
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/views.py39
-rw-r--r--docs/tutorials/wiki/src/basiclayout/.coveragerc3
-rw-r--r--docs/tutorials/wiki/src/basiclayout/.gitignore21
-rw-r--r--docs/tutorials/wiki/src/basiclayout/CHANGES.txt2
-rw-r--r--docs/tutorials/wiki/src/basiclayout/README.txt27
-rw-r--r--docs/tutorials/wiki/src/basiclayout/development.ini17
-rw-r--r--docs/tutorials/wiki/src/basiclayout/production.ini18
-rw-r--r--docs/tutorials/wiki/src/basiclayout/pytest.ini3
-rw-r--r--docs/tutorials/wiki/src/basiclayout/setup.py67
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py15
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/models.py4
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/pshell.py11
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gifbin49 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt126
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/tests.py3
-rw-r--r--docs/tutorials/wiki/src/basiclayout/tutorial/views.py2
-rw-r--r--docs/tutorials/wiki/src/installation/.coveragerc3
-rw-r--r--docs/tutorials/wiki/src/installation/.gitignore21
-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.txt29
-rw-r--r--docs/tutorials/wiki/src/installation/development.ini66
-rw-r--r--docs/tutorials/wiki/src/installation/production.ini60
-rw-r--r--docs/tutorials/wiki/src/installation/pytest.ini3
-rw-r--r--docs/tutorials/wiki/src/installation/setup.py57
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/__init__.py23
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/models.py12
-rw-r--r--docs/tutorials/wiki/src/installation/tutorial/pshell.py11
-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.pt65
-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/.coveragerc3
-rw-r--r--docs/tutorials/wiki/src/models/.gitignore21
-rw-r--r--docs/tutorials/wiki/src/models/CHANGES.txt2
-rw-r--r--docs/tutorials/wiki/src/models/README.txt27
-rw-r--r--docs/tutorials/wiki/src/models/development.ini17
-rw-r--r--docs/tutorials/wiki/src/models/production.ini18
-rw-r--r--docs/tutorials/wiki/src/models/pytest.ini3
-rw-r--r--docs/tutorials/wiki/src/models/setup.py67
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/__init__.py15
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/models.py4
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/pshell.py11
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/static/transparent.gifbin49 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt126
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/tests.py46
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/views.py2
-rw-r--r--docs/tutorials/wiki/src/tests/.coveragerc3
-rw-r--r--docs/tutorials/wiki/src/tests/.gitignore21
-rw-r--r--docs/tutorials/wiki/src/tests/CHANGES.txt3
-rw-r--r--docs/tutorials/wiki/src/tests/README.txt27
-rw-r--r--docs/tutorials/wiki/src/tests/development.ini17
-rw-r--r--docs/tutorials/wiki/src/tests/production.ini18
-rw-r--r--docs/tutorials/wiki/src/tests/pytest.ini3
-rw-r--r--docs/tutorials/wiki/src/tests/setup.py69
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/__init__.py19
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/models.py4
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/pshell.py11
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/security.py17
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/static/transparent.gifbin49 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt120
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/templates/login.pt119
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt73
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/templates/view.pt121
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/tests.py15
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/views.py39
-rw-r--r--docs/tutorials/wiki/src/views/.coveragerc3
-rw-r--r--docs/tutorials/wiki/src/views/.gitignore21
-rw-r--r--docs/tutorials/wiki/src/views/CHANGES.txt5
-rw-r--r--docs/tutorials/wiki/src/views/README.txt27
-rw-r--r--docs/tutorials/wiki/src/views/development.ini17
-rw-r--r--docs/tutorials/wiki/src/views/production.ini18
-rw-r--r--docs/tutorials/wiki/src/views/pytest.ini3
-rw-r--r--docs/tutorials/wiki/src/views/setup.py67
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/__init__.py15
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/models.py4
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/pshell.py11
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/static/transparent.gifbin49 -> 0 bytes
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/templates/edit.pt115
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt76
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/templates/view.pt121
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/tests.py127
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/views.py12
-rw-r--r--docs/tutorials/wiki/tests.rst94
-rw-r--r--docs/tutorials/wiki2/authentication.rst312
-rw-r--r--docs/tutorials/wiki2/authorization.rst524
-rw-r--r--docs/tutorials/wiki2/background.rst18
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst397
-rw-r--r--docs/tutorials/wiki2/definingmodels.rst420
-rw-r--r--docs/tutorials/wiki2/definingviews.rst579
-rw-r--r--docs/tutorials/wiki2/design.rst259
-rw-r--r--docs/tutorials/wiki2/distributing.rst50
-rw-r--r--docs/tutorials/wiki2/index.rst15
-rw-r--r--docs/tutorials/wiki2/installation.rst637
-rw-r--r--docs/tutorials/wiki2/src/authentication/.coveragerc3
-rw-r--r--docs/tutorials/wiki2/src/authentication/.gitignore21
-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.txt43
-rw-r--r--docs/tutorials/wiki2/src/authentication/development.ini82
-rw-r--r--docs/tutorials/wiki2/src/authentication/production.ini76
-rw-r--r--docs/tutorials/wiki2/src/authentication/pytest.ini3
-rw-r--r--docs/tutorials/wiki2/src/authentication/setup.py63
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/__init__.py13
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py78
-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.py28
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/pshell.py12
-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/initialize_db.py56
-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.jinja264
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/tests.py65
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py46
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/default.py79
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/authorization/.coveragerc3
-rw-r--r--docs/tutorials/wiki2/src/authorization/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/authorization/CHANGES.txt2
-rw-r--r--docs/tutorials/wiki2/src/authorization/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/authorization/README.txt41
-rw-r--r--docs/tutorials/wiki2/src/authorization/development.ini25
-rw-r--r--docs/tutorials/wiki2/src/authorization/production.ini36
-rw-r--r--docs/tutorials/wiki2/src/authorization/pytest.ini3
-rw-r--r--docs/tutorials/wiki2/src/authorization/setup.py73
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/__init__.py36
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models.py37
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py78
-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.py28
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/pshell.py12
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/routes.py56
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py37
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/security.py45
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/static/transparent.gifbin49 -> 0 bytes
-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.pt62
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja264
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja226
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt58
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt76
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt65
-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/.coveragerc3
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/CHANGES.txt2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/README.txt41
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/development.ini23
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/production.ini26
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/pytest.ini3
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/setup.py70
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py19
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models.py24
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py77
-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/pshell.py12
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py3
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initialize_db.py48
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py37
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/static/transparent.gifbin49 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja264
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja28
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt73
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py68
-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/models/tutorial/views.py)23
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/installation/.coveragerc3
-rw-r--r--docs/tutorials/wiki2/src/installation/.gitignore21
-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.txt43
-rw-r--r--docs/tutorials/wiki2/src/installation/development.ini80
-rw-r--r--docs/tutorials/wiki2/src/installation/production.ini74
-rw-r--r--docs/tutorials/wiki2/src/installation/pytest.ini3
-rw-r--r--docs/tutorials/wiki2/src/installation/setup.py61
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/__init__.py12
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py77
-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/pshell.py12
-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/initialize_db.py48
-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.jinja264
-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/basiclayout/tutorial/views.py)23
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/models/.coveragerc3
-rw-r--r--docs/tutorials/wiki2/src/models/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/models/CHANGES.txt2
-rw-r--r--docs/tutorials/wiki2/src/models/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/models/README.txt41
-rw-r--r--docs/tutorials/wiki2/src/models/development.ini23
-rw-r--r--docs/tutorials/wiki2/src/models/production.ini34
-rw-r--r--docs/tutorials/wiki2/src/models/pytest.ini3
-rw-r--r--docs/tutorials/wiki2/src/models/setup.py71
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/__init__.py19
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models.py25
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/__init__.py78
-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.py28
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/pshell.py12
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/routes.py3
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py37
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/static/transparent.gifbin49 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja28
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja264
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja28
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt73
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/tests.py68
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/__init__.py0
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/default.py32
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/notfound.py7
-rw-r--r--docs/tutorials/wiki2/src/tests/.coveragerc3
-rw-r--r--docs/tutorials/wiki2/src/tests/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/tests/CHANGES.txt2
-rw-r--r--docs/tutorials/wiki2/src/tests/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/tests/README.txt41
-rw-r--r--docs/tutorials/wiki2/src/tests/development.ini25
-rw-r--r--docs/tutorials/wiki2/src/tests/production.ini36
-rw-r--r--docs/tutorials/wiki2/src/tests/pytest.ini3
-rw-r--r--docs/tutorials/wiki2/src/tests/setup.py74
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/__init__.py36
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models.py37
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py78
-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.py28
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/pshell.py12
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/routes.py56
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py37
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/security.py45
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/static/transparent.gifbin49 -> 0 bytes
-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.pt58
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja264
-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/mytemplate.pt73
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt61
-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.py134
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py16
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py23
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py67
-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/.coveragerc3
-rw-r--r--docs/tutorials/wiki2/src/views/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/views/CHANGES.txt2
-rw-r--r--docs/tutorials/wiki2/src/views/MANIFEST.in2
-rw-r--r--docs/tutorials/wiki2/src/views/README.txt41
-rw-r--r--docs/tutorials/wiki2/src/views/development.ini23
-rw-r--r--docs/tutorials/wiki2/src/views/production.ini34
-rw-r--r--docs/tutorials/wiki2/src/views/pytest.ini3
-rw-r--r--docs/tutorials/wiki2/src/views/setup.py73
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/__init__.py22
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models.py25
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/__init__.py78
-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.py28
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/pshell.py12
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/routes.py6
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py37
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/favicon.icobin1406 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/footerbg.pngbin333 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/headerbg.pngbin203 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/ie6.css8
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/middlebg.pngbin2797 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/pylons.css372
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/pyramid-small.pngbin7044 -> 0 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/pyramid.pngbin33055 -> 12901 bytes
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/theme.css154
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/static/transparent.gifbin49 -> 0 bytes
-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/edit.pt54
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja255
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt73
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja218
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/templates/view.pt57
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/tests.py175
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views.py71
-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.rst138
471 files changed, 12471 insertions, 9621 deletions
diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst
index ddd968927..fa0d4f0cb 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
@@ -9,7 +9,7 @@ server.
This guide will outline broad steps that can be used to get a :app:`Pyramid`
application running under Apache via ``mod_wsgi``. This particular tutorial
-was developed under Apple's Mac OS X platform (Snow Leopard, on a 32-bit
+was developed under Apple's macOS platform (Snow Leopard, on a 32-bit
Mac), but the instructions should be largely the same for all systems, delta
specific path information for commands and files.
@@ -18,80 +18,90 @@ specific path information for commands and files.
``mod_wsgi``. If you have experience with :app:`Pyramid` and ``mod_wsgi``
on Windows systems, please help us document this experience by submitting
documentation to the `Pylons-devel maillist
- <http://groups.google.com/group/pylons-devel>`_.
+ <https://groups.google.com/forum/#!forum/pylons-devel>`_.
#. The tutorial assumes you have Apache already installed on your
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>`_
+ <https://code.google.com/archive/p/modwsgi/wikis/InstallationInstructions.wiki>`_
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
+#. Create a :app:`Pyramid` application using our :term:`cookiecutter`. See
+ :ref:`project_narr` for more in-depth information about creating a new
+ project.
- $ sudo /usr/bin/easy_install-2.6 virtualenv
+ .. code-block:: bash
- This command may need to be performed as the root user.
+ cd ~
+ cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout master
-#. Create a :term:`virtualenv` which we'll use to install our
- application.
+ If prompted for the first item, accept the default ``yes`` by hitting return.
.. code-block:: text
- $ cd ~
- $ mkdir modwsgi
- $ cd modwsgi
- $ /usr/local/bin/virtualenv env
+ You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
+ Is it okay to delete and re-clone it? [yes]: yes
+ project_name [Pyramid Scaffold]: myproject
+ repo_name [myproject]: myproject
+ Select template_language:
+ 1 - jinja2
+ 2 - chameleon
+ 3 - mako
+ Choose from 1, 2, 3 [1]: 1
+ Select backend:
+ 1 - none
+ 2 - sqlalchemy
+ 3 - zodb
+ Choose from 1, 2, 3 [1]: 1
-#. Install :app:`Pyramid` into the newly created virtualenv:
+#. Create a :term:`virtual environment` which we'll use to install our
+ application. It is important to use the same base Python interpreter
+ that was used to build ``mod_wsgi``. For example, if ``mod_wsgi`` was
+ built against the system Python 3.x, then your project should use a
+ virtual environment created from that same system Python 3.x.
- .. code-block:: text
+ .. code-block:: bash
- $ cd ~/modwsgi/env
- $ $VENV/bin/easy_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
- a baseline application. Substitute your existing :app:`Pyramid`
- application as necessary if you already have one.
+ cd myproject
+ python3 -m venv env
- .. code-block:: text
+#. Install your :app:`Pyramid` application and its dependencies.
- $ cd ~/modwsgi/env
- $ $VENV/bin/pcreate -s starter myapp
- $ cd myapp
- $ $VENV/bin/python setup.py install
+ .. code-block:: bash
-#. Within the virtualenv directory (``~/modwsgi/env``), create a
- script named ``pyramid.wsgi``. Give it these contents:
+ env/bin/pip install -e .
+
+#. Within the project directory (``~/myproject``), create a script
+ named ``pyramid.wsgi``. Give it these contents:
.. code-block:: python
- from pyramid.paster import get_app, setup_logging
- ini_path = '/Users/chrism/modwsgi/env/myapp/production.ini'
- setup_logging(ini_path)
- application = get_app(ini_path, 'main')
+ from pyramid.paster import get_app, setup_logging
+ ini_path = '/Users/chrism/myproject/production.ini'
+ setup_logging(ini_path)
+ application = get_app(ini_path, 'main')
- The first argument to ``get_app`` is the project configuration file
- name. It's best to use the ``production.ini`` file provided by your
- scaffold, as it contains settings appropriate for
- production. The second is the name of the section within the .ini file
- that should be loaded by ``mod_wsgi``. The assignment to the name
+ The first argument to :func:`pyramid.paster.get_app` is the project
+ configuration file name. It's best to use the ``production.ini`` file
+ provided by your cookiecutter, as it contains settings appropriate for
+ production. The second is the name of the section within the ``.ini``
+ file that should be loaded by ``mod_wsgi``. The assignment to the name
``application`` is important: mod_wsgi requires finding such an
assignment when it opens the file.
- The call to ``setup_logging`` initializes the standard library's
- `logging` module to allow logging within your application.
+ The call to :func:`pyramid.paster.setup_logging` initializes the standard
+ library's `logging` module to allow logging within your application.
See :ref:`logging_config`.
There is no need to make the ``pyramid.wsgi`` script executable.
However, you'll need to make sure that *two* users have access to change
- into the ``~/modwsgi/env`` directory: your current user (mine is
+ into the ``~/myproject`` directory: your current user (mine is
``chrism`` and the user that Apache will run as often named ``apache`` or
``httpd``). Make sure both of these users can "cd" into that directory.
@@ -101,34 +111,31 @@ specific path information for commands and files.
.. code-block:: apache
- # Use only 1 Python sub-interpreter. Multiple sub-interpreters
- # play badly with C extensions. See
- # http://stackoverflow.com/a/10558360/209039
- WSGIApplicationGroup %{GLOBAL}
- WSGIPassAuthorization On
- WSGIDaemonProcess pyramid user=chrism group=staff threads=4 \
- python-path=/Users/chrism/modwsgi/env/lib/python2.6/site-packages
- WSGIScriptAlias /myapp /Users/chrism/modwsgi/env/pyramid.wsgi
+ # Use only 1 Python sub-interpreter. Multiple sub-interpreters
+ # play badly with C extensions. See
+ # http://stackoverflow.com/a/10558360/209039
+ WSGIApplicationGroup %{GLOBAL}
+ WSGIPassAuthorization On
+ WSGIDaemonProcess pyramid user=chrism group=staff threads=4 \
+ python-path=/Users/chrism/myproject/env/lib/python3.5/site-packages
+ WSGIScriptAlias /myapp /Users/chrism/myproject/pyramid.wsgi
- <Directory /Users/chrism/modwsgi/env>
+ <Directory /Users/chrism/myproject>
WSGIProcessGroup pyramid
- Order allow,deny
- Allow from all
- </Directory>
-
+ Require all granted
+ </Directory>
+
#. Restart Apache
- .. code-block:: text
+ .. code-block:: bash
- $ sudo /usr/sbin/apachectl restart
+ sudo /usr/sbin/apachectl restart
#. Visit ``http://localhost/myapp`` in a browser. You should see the
sample application rendered in your browser.
-:term:`mod_wsgi` has many knobs and a great variety of deployment
-modes. This is just one representation of how you might use it to
-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.
-
+:term:`mod_wsgi` has many knobs and a great variety of deployment modes. This
+is just one representation of how you might use it to serve up a :app:`Pyramid`
+application. See the `mod_wsgi configuration documentation
+<https://modwsgi.readthedocs.io/en/develop/configuration.html>`_
+for more in-depth configuration information.
diff --git a/docs/tutorials/wiki/NOTE-relocatable.txt b/docs/tutorials/wiki/NOTE-relocatable.txt
index e942caba8..c3f30af51 100644
--- a/docs/tutorials/wiki/NOTE-relocatable.txt
+++ b/docs/tutorials/wiki/NOTE-relocatable.txt
@@ -1,6 +1,7 @@
We specifically use relative package references where possible so this demo
-works even if the user names their package (in the '$VENV/bin/pcreate -s
-zodb ...' step) something other than 'tutorial'.
+works even if the user names their package (in the
+'cookiecutter gh:Pylons/pyramid-cookiecutter-starter' step) something other
+than 'tutorial'.
Specifically:
@@ -9,5 +10,5 @@ Specifically:
page templates.
Direct uses of the package name, like in __init__.py 'config.scan()'
-statements, are already adjusted by the paster/pcreate, so we don't have to
+statements, are already adjusted by the cookiecutter, so we don't have to
worry about them.
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index 93cd0c18e..b7eeb19ae 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -1,22 +1,24 @@
+.. _wiki_adding_authorization:
+
====================
-Adding Authorization
+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 a permission, instead of
-a default "403 Forbidden" page.
+: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 password hashing dependencies.
* Add users and groups (``security.py``, a new module).
* Add an :term:`ACL` (``models.py``).
* Add an :term:`authentication policy` and an :term:`authorization policy`
@@ -24,23 +26,45 @@ We will implement the access control with the following steps:
* Add :term:`permission` declarations to the ``edit_page`` and ``add_page``
views (``views.py``).
-Then we will add the login and logout feature:
+Then we will add the login and logout features:
* 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``).
+* 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
+Access control
--------------
+
+Add dependencies
+~~~~~~~~~~~~~~~~
+
+Just like in :ref:`wiki_defining_views`, we need a new dependency. We need to add the `bcrypt <https://pypi.org/project/bcrypt/>`_ package, to our tutorial package's ``setup.py`` file by assigning this dependency to the ``requires`` parameter in the ``setup()`` function.
+
+Open ``setup.py`` and edit it to look like the following:
+
+.. literalinclude:: src/authorization/setup.py
+ :linenos:
+ :emphasize-lines: 23
+ :language: python
+
+Only the highlighted line needs to be added.
+
+Do not forget to run ``pip install -e .`` just like in :ref:`wiki-running-pip-install`.
+
+.. note::
+
+ 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.
+
+
Add users and groups
~~~~~~~~~~~~~~~~~~~~
-Create a new ``tutorial/tutorial/security.py`` module with the
-following content:
+Create a new ``tutorial/security.py`` module with the following content:
.. literalinclude:: src/authorization/tutorial/security.py
:linenos:
@@ -49,11 +73,9 @@ following content:
The ``groupfinder`` function accepts a userid and a request and
returns one of these values:
-- If the userid exists in the system, it will return a
- sequence of group identifiers (or an empty sequence if the user
- isn't a member of any groups).
-- If the userid *does not* exist in the system, it will
- return ``None``.
+- If ``userid`` exists in the system, it will return a sequence of group
+ identifiers (or an empty sequence if the user isn't a member of any groups).
+- If the userid *does not* exist in the system, it will return ``None``.
For example, ``groupfinder('editor', request )`` returns ``['group:editor']``,
``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin',
@@ -61,80 +83,89 @@ 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.
+There are two helper methods that will help us later to authenticate users.
+The first is ``hash_password`` which takes a raw password and transforms 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. 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 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.
+
+In a production system, user and group data will most often be saved and come from a
+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
-statement at the head:
+Open ``tutorial/models.py`` and add the following import
+statement near the top:
.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 4-7
- :linenos:
+ :lines: 4-8
+ :lineno-match:
:language: python
Add the following lines to the ``Wiki`` class:
.. literalinclude:: src/authorization/tutorial/models.py
:lines: 9-13
- :linenos:
+ :lineno-match:
:emphasize-lines: 4-5
: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
+We import :data:`~pyramid.security.Allow`, an action that means that
+permission is allowed, and :data:`~pyramid.security.Everyone`, a special
+:term:`principal` that is associated to all requests. Both are used in the
:term:`ACE` entries that make up the ACL.
-The ACL is a list that needs to be named `__acl__` and be an
-attribute of a class. We define an :term:`ACL` with two
-:term:`ACE` entries: the first entry allows any user the `view`
-permission, and the second entry allows the ``group:editors``
-principal the `edit` permission.
+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.
-The ``Wiki`` class that contains the ACL is the :term:`resource`
-constructor for the :term:`root` resource, which is
-a ``Wiki`` instance. The ACL is
-provided to each view in the :term:`context` of the request, as
-the ``context`` attribute.
+The ``Wiki`` class that contains the ACL is the :term:`resource` constructor
+for the :term:`root` resource, which is a ``Wiki`` instance. The ACL is
+provided to each view in the :term:`context` of the request as the ``context``
+attribute.
It's only happenstance that we're assigning this ACL at class scope. An ACL
can be attached to an object *instance* too; this is how "row level security"
can be achieved in :app:`Pyramid` applications. We actually need only *one*
ACL for the entire system, however, because our security requirements are
-simple, so this feature is not demonstrated. See
-:ref:`assigning_acls` for more information about what an
-:term:`ACL` represents.
+simple, so this feature is not demonstrated. See :ref:`assigning_acls` for
+more information about what an :term:`ACL` represents.
-Add Authentication and Authorization Policies
+Add authentication and authorization policies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/__init__.py`` and
-add these import statements:
+Open ``tutorial/__init__.py`` and add the highlighted import
+statements:
.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 4-5,8
+ :lines: 1-8
:linenos:
+ :emphasize-lines: 3-6,8
:language: python
Now add those policies to the configuration:
.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 18-23
- :linenos:
- :emphasize-lines: 1-3,5-6
+ :lines: 18-25
+ :lineno-match:
+ :emphasize-lines: 2-4,6-7
:language: python
-(Only the highlighted lines need to be added.)
+Only the highlighted lines need to be added.
-We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an
-auth ticket that may be included in the request, and an
-``ACLAuthorizationPolicy`` that uses an ACL to determine the allow or deny
-outcome for a view.
+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
@@ -144,235 +175,230 @@ machinery represented by this policy: it is required. The ``callback`` is the
Add permission declarations
~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/tutorial/views.py``. Add a ``permission='edit'`` parameter
-to the ``@view_config`` decorator for ``add_page()`` and
-``edit_page()``, for example:
+Open ``tutorial/views.py`` and add a ``permission='edit'`` parameter
+to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
-.. code-block:: python
- :linenos:
- :emphasize-lines: 3
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 49-51
+ :emphasize-lines: 2-3
+ :language: python
- @view_config(name='add_page', context='.models.Wiki',
- renderer='templates/edit.pt',
- permission='edit')
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 68-70
+ :emphasize-lines: 2-3
+ :language: python
-(Only the highlighted line, along with its preceding comma,
-needs to be added.)
+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 result is that only users who possess the ``edit`` permission at the time
+of the request may invoke those two views.
-Add a ``permission='view'`` parameter to the ``@view_config``
-decorator for ``view_wiki()`` and ``view_page()``, like this:
+Add a ``permission='view'`` parameter to the ``@view_config`` decorator for
+``view_wiki()`` and ``view_page()`` as follows:
-.. code-block:: python
- :linenos:
- :emphasize-lines: 2
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 23-24
+ :emphasize-lines: 1-2
+ :language: python
- @view_config(context='.models.Page', renderer='templates/view.pt',
- permission='view')
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 28-29
+ :emphasize-lines: 1-2
+ :language: python
-(Only the highlighted line, along with its preceding comma,
-needs to be added.)
+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.
+We are done with the changes needed to control access. The changes that
+follow will add the login and logout feature.
-Login, Logout
+Login, logout
-------------
-Add Login and Logout Views
+Add login and logout views
~~~~~~~~~~~~~~~~~~~~~~~~~~
-We'll add a ``login`` view which renders a login form and processes
-the post from the login form, checking credentials.
+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.
+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``:
+Add the following import statements to the head of
+``tutorial/views.py``:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 6-13,15-17
- :linenos:
- :emphasize-lines: 3,6-9,11
+ :lines: 6-17
+ :emphasize-lines: 1-12
:language: python
-(Only the highlighted lines, with other necessary modifications,
-need to be added.)
+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.
+: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:
+Now add the ``login`` and ``logout`` views at the end of the file:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 82-120
- :linenos:
+ :lines: 80-
+ :lineno-match:
: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.
+- 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.
-The order of these two :term:`view configuration` decorators
-is unimportant.
+The order of these two :term:`view configuration` decorators is unimportant.
-``logout()`` is decorated with a ``@view_config`` decorator
-which associates it with the ``logout`` route. It will be
-invoked when we visit ``/logout``.
+``logout()`` is decorated with a ``@view_config`` decorator which associates
+it with the ``logout`` route. It will be invoked when we visit ``/logout``.
Add the ``login.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: xml
+ :language: html
-The above template is referred in the login view that we just added
-in ``views.py``.
+The above template is referenced in the login view that we just added in
+``views.py``.
-Return a logged_in flag to the renderer
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Return a ``logged_in`` flag to the renderer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Add a ``logged_in`` parameter to the return value of
-``view_page()``, ``edit_page()`` and ``add_page()``,
-like this:
+Open ``tutorial/views.py`` again. Add a ``logged_in`` parameter to
+the return value of ``view_page()``, ``add_page()``, and ``edit_page()`` as
+follows:
-.. code-block:: python
- :linenos:
- :emphasize-lines: 4
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 46-47
+ :emphasize-lines: 1-2
+ :language: python
+
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 65-66
+ :emphasize-lines: 1-2
+ :language: python
- return dict(page = page,
- content = content,
- edit_url = edit_url,
- logged_in = request.authenticated_userid)
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 76-78
+ :emphasize-lines: 2-3
+ :language: python
-(Only the highlighted line and a trailing comma on the preceding
-line need to be added.)
+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 user id if the user is authenticated.
+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 this within the
-``<div id="right" class="app-welcome align-right">`` div:
+Open ``tutorial/templates/edit.pt`` and
+``tutorial/templates/view.pt`` and add the following code as
+indicated by the highlighted lines.
-.. code-block:: xml
-
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
+.. literalinclude:: src/authorization/tutorial/templates/edit.pt
+ :lines: 35-39
+ :emphasize-lines: 2-4
+ :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.
+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.
-Seeing Our Changes
-------------------
+Reviewing our changes
+---------------------
-Our ``tutorial/tutorial/__init__.py`` will look something 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:
- :emphasize-lines: 4-5,8,18-20,22-23
+ :emphasize-lines: 4-5,8,19-21,23-24
:language: python
-(Only the highlighted lines need to be added.)
+Only the highlighted lines need to be added or edited.
-Our ``tutorial/tutorial/models.py`` will look something 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:
:emphasize-lines: 4-7,12-13
:language: python
-(Only the highlighted lines need to be added.)
+Only the highlighted lines need to be added or edited.
-Our ``tutorial/tutorial/views.py`` will look something 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:
- :emphasize-lines: 8,11-15,17,24,29,48,52,68,72,80,82-120
+ :emphasize-lines: 8,11-15,17,24,29,47,51,66,70,78,80-
:language: python
-(Only the highlighted lines need to be added.)
+Only the highlighted lines need to be added or edited.
-Our ``tutorial/tutorial/templates/edit.pt`` template will look
-something like this when we're done:
+Our ``tutorial/templates/edit.pt`` template will look like this when
+we're done:
.. literalinclude:: src/authorization/tutorial/templates/edit.pt
:linenos:
- :emphasize-lines: 41-43
- :language: xml
+ :emphasize-lines: 36-38
+ :language: html
-(Only the highlighted lines need to be added.)
+Only the highlighted lines need to be added or edited.
-Our ``tutorial/tutorial/templates/view.pt`` template will look
-something like this when we're done:
+Our ``tutorial/templates/view.pt`` template will look like this when
+we're done:
.. literalinclude:: src/authorization/tutorial/templates/view.pt
:linenos:
- :emphasize-lines: 41-43
- :language: xml
+ :emphasize-lines: 36-38
+ :language: html
-(Only the highlighted lines need to be added.)
+Only the highlighted lines need to be added or edited.
-Viewing the Application in a Browser
+Viewing the application in a browser
------------------------------------
We can finally examine our application in a browser (See
-:ref:`wiki-start-the-application`). Launch a browser and visit
-each of the following URLs, check 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 resource. It is executable by any user.
-
-- ``http://localhost:6543/FrontPage`` invokes
- the ``view_page`` view of the ``FrontPage`` Page resource. This is because
- it's the :term:`default view` (a view without a ``name``) for ``Page``
- resources. It is executable by any user.
-
-- ``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.
-
-- 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.
+:ref:`wiki-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 resource. It
+ is executable by any user.
+
+- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
+ ``FrontPage`` Page resource. This is because it's the :term:`default view`
+ (a view without a ``name``) for ``Page`` resources. It is executable by any
+ user.
+
+- 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.
+
+- After logging in (as a result of hitting an edit or add page and submitting
+ the login form with the ``editor`` credentials), we'll see a Logout link in
+ the upper right hand corner. When we click it, we're logged out, and
+ redirected back to the front page.
diff --git a/docs/tutorials/wiki/background.rst b/docs/tutorials/wiki/background.rst
index 6bbd5026e..c10ab9e55 100644
--- a/docs/tutorials/wiki/background.rst
+++ b/docs/tutorials/wiki/background.rst
@@ -1,3 +1,5 @@
+.. _wiki_background:
+
==========
Background
==========
@@ -9,9 +11,9 @@ familiar to someone with :term:`Zope` experience. It uses
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.
+To code along with this tutorial, the developer will need a Unix
+machine with development tools (macOS with XCode, any Linux or BSD
+variant, and so on) *or* a Windows system of any kind.
.. warning::
diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst
index cdf52b73e..49ee6902e 100644
--- a/docs/tutorials/wiki/basiclayout.rst
+++ b/docs/tutorials/wiki/basiclayout.rst
@@ -1,44 +1,60 @@
+.. _wiki_basic_layout:
+
============
Basic Layout
============
-The starter files generated by the ``zodb`` scaffold are basic, but
-they provide a good orientation for the high-level patterns common to most
-:term:`traversal` -based :app:`Pyramid` (and :term:`ZODB` -based) projects.
+The starter files generated by selecting the ``zodb`` backend in the
+cookiecutter are very basic, but they provide a good orientation for the
+high-level patterns common to most :term:`traversal`-based (and
+:term:`ZODB`-based) :app:`Pyramid` projects.
-Application Configuration with ``__init__.py``
-------------------------------------------------
+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. Our application uses ``__init__.py`` both as a package marker and
-to contain application configuration code.
+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 config file, the application 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``:
+``development.ini`` generated configuration file, the application
+configuration points at a :term:`Setuptools` :term:`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``.
+
+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.
-#. *Lines 6-8*. Define a root factory for our Pyramid application.
+#. *Lines 6-8*. Define a :term:`root factory` for our Pyramid application.
+
+#. *Line 11*. ``__init__.py`` defines a function named ``main``.
+
+#. *Line 14*. Use an explicit transaction manager for apps so that they do not implicitly create new transactions when touching the manager outside of the ``pyramid_tm`` lifecycle.
-#. *Line 14*. We construct a :term:`Configurator` with a :term:`root
- factory` and the settings keywords parsed by :term:`PasteDeploy`. The root
- factory is named ``root_factory``.
+#. *Line 15*. Construct a :term:`Configurator` as a :term:`context manager` with the settings keyword parsed by :term:`PasteDeploy`.
-#. *Line 15*. Include support for the :term:`Chameleon` template rendering
+#. *Line 16*. Include support for the :term:`Chameleon` template rendering
bindings, allowing us to use the ``.pt`` templates.
-#. *Line 16*. Register a "static view" which answers requests whose URL path
- start with ``/static`` using the
+#. *Line 17*. Include support for ``pyramid_tm``, allowing Pyramid requests to join the active transaction as provided by the `transaction <https://pypi.org/project/transaction/>`_ package.
+
+#. *Line 18*. Include support for ``pyramid_retry`` to retry a request when transient exceptions occur.
+
+#. *Line 19*. Include support for ``pyramid_zodbconn``, providing integration between :term:`ZODB` and a Pyramid application.
+
+#. *Line 20*. Set a root factory using our function named ``root_factory``.
+
+#. *Line 21*. Register a "static view", which answers requests whose URL
+ paths start with ``/static``, using the
:meth:`pyramid.config.Configurator.add_static_view` method. This
statement registers a view that will serve up static assets, such as CSS
and image files, for us, in this case, at
@@ -47,23 +63,23 @@ point happens to be the ``main`` function within the file named
will be ``/static``. The second argument of this tag is the "path",
which is a relative :term:`asset specification`, so it finds the resources
it should serve within the ``static`` directory inside the ``tutorial``
- package. Alternatively the scaffold could have used an *absolute* asset
+ package. Alternatively the cookiecutter could have used an *absolute* asset
specification as the path (``tutorial:static``).
-#. *Line 17*. Perform a :term:`scan`. A scan will find :term:`configuration
+#. *Line 22*. Perform a :term:`scan`. A scan will find :term:`configuration
decoration`, such as view configuration decorators (e.g., ``@view_config``)
in the source code of the ``tutorial`` package and will take actions based
on these decorators. We don't pass any arguments to
:meth:`~pyramid.config.Configurator.scan`, which implies that the scan
should take place in the current package (in this case, ``tutorial``).
- The scaffold could have equivalently said ``config.scan('tutorial')``, but
+ The cookiecutter could have equivalently said ``config.scan('tutorial')``, but
it chose to omit the package name argument.
-#. *Line 18*. Use the
+#. *Line 23*. Use the
:meth:`pyramid.config.Configurator.make_wsgi_app` method
to return a :term:`WSGI` application.
-Resources and Models with ``models.py``
+Resources and models with ``models.py``
---------------------------------------
:app:`Pyramid` uses the word :term:`resource` to describe objects arranged
@@ -72,47 +88,46 @@ hierarchically in a :term:`resource tree`. This tree is consulted by
tree represents the site structure, but it *also* represents the
:term:`domain model` of the application, because each resource is a node
stored persistently in a :term:`ZODB` database. The ``models.py`` file is
-where the ``zodb`` scaffold put the classes that implement our
+where the ``zodb`` cookiecutter put the classes that implement our
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`
because the class inherits from the
:class:`persistent.mapping.PersistentMapping` class. The ``__parent__``
and ``__name__`` are important parts of the :term:`traversal` protocol.
- By default, have these as ``None`` indicating that this is the
+ By default, set these to ``None`` to indicate that this is the
:term:`root` object.
-#. *Lines 8-14*. ``appmaker`` is used to return the *application
+#. *Lines 8-12*. ``appmaker`` is used to return the *application
root* object. It is called on *every request* to the
:app:`Pyramid` application. It also performs bootstrapping by
*creating* an application root (inside the ZODB root object) if one
- does not already exist. It is used by the "root_factory" we've defined
+ does not already exist. It is used by the ``root_factory`` we've defined
in our ``__init__.py``.
- We do so by first seeing if the database has the persistent
- application root. If not, we make an instance, store it, and
- commit the transaction. We then return the application root
- object.
+ Bootstrapping is done by first seeing if the database has the persistent
+ application root. If not, we make an instance, store it, and commit the
+ transaction. We then return the application root object.
Views With ``views.py``
-----------------------
-Our scaffold generated a default ``views.py`` on our behalf. It
+Our cookiecutter generated a default ``views.py`` on our behalf. It
contains a single view, which is used to render the page shown when you visit
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:
@@ -150,7 +165,7 @@ Let's try to understand the components in this module:
#. *Lines 6-7*. We define a :term:`view callable` named ``my_view``, which
we decorated in the step above. This view callable is a *function* we
- write generated by the ``zodb`` scaffold that is given a
+ write generated by the ``zodb`` cookiecutter that is given a
``request`` and which returns a dictionary. The ``mytemplate.pt``
:term:`renderer` named by the asset specification in the step above will
convert this dictionary to a :term:`response` on our behalf.
@@ -162,15 +177,15 @@ Let's try to understand the components in this module:
Configuration in ``development.ini``
------------------------------------
-The ``development.ini`` (in the tutorial :term:`project` directory, as
-opposed to the tutorial :term:`package` directory) looks like this:
+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
``zodbconn.uri`` setting within this section. This value, and the other
-values within this section are passed as ``**settings`` to the ``main``
+values within this section, are passed as ``**settings`` to the ``main``
function we defined in ``__init__.py`` when the server is started via
``pserve``.
diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst
index 49372179f..e973cfdfe 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 cookiecutter-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.
@@ -15,7 +17,7 @@ single instance of the "Wiki" class will serve as a container for "Page"
objects, which will be instances of the "Page" class.
-Delete the Database
+Delete the database
-------------------
In the next step, we're going to remove the ``MyModel`` Python model
@@ -32,17 +34,25 @@ Edit ``models.py``
.. note::
- There is nothing automagically special about the filename ``models.py``. A
+ 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,
+ 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.
+Open ``tutorial/models.py`` file and edit it to look like the following:
+
+.. literalinclude:: src/models/tutorial/models.py
+ :linenos:
+ :language: python
+
The first thing we want to do is remove the ``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'll add a ``Wiki`` class. We want it to inherit from the
+Then we'll add an import at the top for the :class:`persistent.Persistent` class. We'll use this for a new ``Page`` class in a moment.
+
+Then we'll add a ``Wiki`` class. We want it to inherit from the
:class:`persistent.mapping.PersistentMapping` class because it provides
mapping behavior, and it makes sure that our Wiki page is stored as a
"first-class" persistent object in our ZODB database.
@@ -70,17 +80,7 @@ front page) into the Wiki within the ``appmaker``. This will provide
:term:`traversal` a :term:`resource tree` to work against when it attempts to
resolve URLs to resources.
-Look at the Result of Our Edits to ``models.py``
-------------------------------------------------
-
-The result of all of our edits to ``models.py`` will end up looking
-something like this:
-
-.. literalinclude:: src/models/tutorial/models.py
- :linenos:
- :language: python
-
-View the Application in a Browser
+View the application in a browser
---------------------------------
We can't. At this point, our system is in a "non-runnable" state; we'll need
@@ -91,6 +91,6 @@ up with a Python traceback on your console that ends with this exception:
.. code-block:: text
- ImportError: cannot import name MyModel
+ ImportError: cannot import name MyModel
This will also happen if you attempt to run the tests.
diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst
index e06468267..d584a1b41 100644
--- a/docs/tutorials/wiki/definingviews.rst
+++ b/docs/tutorials/wiki/definingviews.rst
@@ -1,8 +1,10 @@
+.. _wiki_defining_views:
+
==============
Defining Views
==============
-A :term:`view callable` in a :term:`traversal` -based :app:`Pyramid`
+A :term:`view callable` in a :term:`traversal`-based :app:`Pyramid`
application is typically a simple Python function that accepts two
parameters: :term:`context` and :term:`request`. A view callable is
assumed to return a :term:`response` object.
@@ -14,10 +16,10 @@ assumed to return a :term:`response` object.
this one-argument pattern used in other :app:`Pyramid` tutorials
and applications. Either calling convention will work in any
:app:`Pyramid` application; the calling conventions can be used
- interchangeably as necessary. In :term:`traversal` based applications,
+ interchangeably as necessary. In :term:`traversal`-based applications,
URLs are mapped to a context :term:`resource`, and since our
:term:`resource tree` also represents our application's
- "domain model", we're often interested in the context, because
+ "domain model", we're often interested in the context because
it represents the persistent storage of our application. For
this reason, in this tutorial we define views as callables that
accept ``context`` in the callable argument list. If you do
@@ -34,36 +36,86 @@ 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. We need to add a dependency on
-the ``docutils`` package to our ``tutorial`` package's ``setup.py`` file by
-assigning this dependency to the ``install_requires`` parameter in the
-``setup`` function.
+application was generated by the cookiecutter; it doesn't know
+about our custom application requirements.
+
+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.
-Our resulting ``setup.py`` should look like so:
+Open ``setup.py`` and edit it to look like the following:
.. literalinclude:: src/views/setup.py
- :linenos:
- :language: python
+ :linenos:
+ :emphasize-lines: 22
+ :language: python
+
+Only the highlighted line needs to be added.
+
+.. _wiki-running-pip-install:
+
+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 .
-.. note:: After these new dependencies are added, you will need to
- rerun ``python setup.py develop`` inside the root of the
- ``tutorial`` package to obtain and register the newly added
- dependency package.
+On Windows:
-Adding View Functions
-=====================
+.. code-block:: doscon
+
+ cd tutorial
+ %VENV%\Scripts\pip install -e .
+
+Success executing this command will end with a line to the console something
+like:
+
+.. code-block:: text
+
+ Successfully installed docutils-0.13.1 tutorial
+
+
+Adding view functions in ``views.py``
+=====================================
+
+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:
+ :language: python
-We're going to add four :term:`view callable` functions to our ``views.py``
-module. One view named ``view_wiki`` will display the wiki itself (it will
-answer on the root URL), another named ``view_page`` will display an
-individual page, another named ``add_page`` will allow a page to be added,
-and a final view named ``edit_page`` will allow a page to be edited.
+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 originally rendered after we selected the ``zodb`` backend option in the
+cookiecutter. It was only an example and isn't relevant to our application.
+
+Then we added four :term:`view callable` functions to our ``views.py``
+module:
+
+* ``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.
+
+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
+ 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.
@@ -71,44 +123,53 @@ and a final view named ``edit_page`` will allow a page to be edited.
The ``view_wiki`` view function
-------------------------------
-Here is the code for the ``view_wiki`` view function and its decorator, which
-will be added to ``views.py``:
+Following is the code for the ``view_wiki`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views.py
- :lines: 12-14
- :language: python
-
-The ``view_wiki`` function will be configured to respond as the default view
-callable for a Wiki resource. We'll provide it with a ``@view_config``
-decorator which names the class ``tutorial.models.Wiki`` as its context.
-This means that when a Wiki resource is the context, and no :term:`view name`
-exists in the request, this view will be used. The view configuration
-associated with ``view_wiki`` does not use a ``renderer`` because the view
-callable always returns a :term:`response` object rather than a dictionary.
-No renderer is necessary when a view returns a response object.
-
-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
+ :lines: 12-14
+ :lineno-match:
+ :language: python
+
+.. note:: In our code, we use an *import* that is *relative* to our package
+ named ``tutorial``, meaning we can omit the name of the package in the
+ ``import`` and ``context`` statements. In our narrative, however, we refer
+ to a *class* and thus we use the *absolute* form, meaning that the name of
+ the package is included.
+
+``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
+represents the path to our "FrontPage".
+
+We provide it with a ``@view_config`` decorator which names the class
+``tutorial.models.Wiki`` as its context. This means that when a Wiki resource
+is the context and no :term:`view name` exists in the request, then this view
+will be used. The view configuration associated with ``view_wiki`` does not
+use a ``renderer`` because the view callable always returns a :term:`response`
+object rather than a dictionary. No renderer is necessary when a view returns
+a response object.
+
+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).
-:meth:`pyramid.request.Request.resource_url` constructs a URL to the
+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
``FrontPage`` page resource (i.e., ``http://localhost:6543/FrontPage``), and
-uses it as the "location" of the HTTPFound response, forming an HTTP
+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, which
-will be added to ``views.py``:
+Here is the code for the ``view_page`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views.py
- :lines: 16-33
- :language: python
+ :lines: 16-33
+ :lineno-match:
+ :language: python
-The ``view_page`` function will be configured to respond as the default view
-of a Page resource. We'll provide it with a ``@view_config`` decorator which
+The ``view_page`` function is configured to respond as the default view
+of a Page resource. We provide it with a ``@view_config`` decorator which
names the class ``tutorial.models.Page`` as its context. This means that
when a Page resource is the context, and no :term:`view name` exists in the
request, this view will be used. We inform :app:`Pyramid` this view will use
@@ -116,9 +177,9 @@ the ``templates/view.pt`` template file as a ``renderer``.
The ``view_page`` function generates the :term:`reStructuredText` body of a
page (stored as the ``data`` attribute of the context passed to the view; the
-context will be a Page resource) as HTML. Then it substitutes an HTML anchor
-for each *WikiWord* reference in the rendered HTML using a compiled regular
-expression.
+context will be a ``Page`` resource) 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
``wikiwords.sub``, indicating that it should be called to provide a value for
@@ -133,8 +194,8 @@ 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 resource.
-We then generate an edit URL (because it's easier to do here than in the
-template), and we wrap up a number of arguments in a dictionary and return
+We then generate an edit URL because it's easier to do here than in the
+template, and we wrap up a number of arguments in a dictionary and return
it.
The arguments we wrap into a dictionary include ``page``, ``content``, and
@@ -153,22 +214,22 @@ callable. In the ``view_wiki`` view callable, we unconditionally return a
The ``add_page`` view function
------------------------------
-Here is the code for the ``add_page`` view function and its decorator, which
-will be added to ``views.py``:
+Here is the code for the ``add_page`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views.py
- :lines: 35-50
- :language: python
-
-The ``add_page`` function will be configured to respond when the context
-resource is a Wiki and the :term:`view name` is ``add_page``. We'll provide
-it with a ``@view_config`` decorator which names the string ``add_page`` as
-its :term:`view name` (via name=), the class ``tutorial.models.Wiki`` as its
-context, and the renderer named ``templates/edit.pt``. This means that when
-a Wiki resource is the context, and a :term:`view name` named ``add_page``
+ :lines: 35-50
+ :lineno-match:
+ :language: python
+
+The ``add_page`` function is configured to respond when the context resource
+is a Wiki and the :term:`view name` is ``add_page``. We provide it with a
+``@view_config`` decorator which names the string ``add_page`` as its
+:term:`view name` (via ``name=``), the class ``tutorial.models.Wiki`` as its
+context, and the renderer named ``templates/edit.pt``. This means that when a
+Wiki resource is the context, and a :term:`view name` named ``add_page``
exists as the result of traversal, this view will be used. We inform
-:app:`Pyramid` this view will use the ``templates/edit.pt`` template file as
-a ``renderer``. We share the same template between add and edit views, thus
+:app:`Pyramid` this view will use the ``templates/edit.pt`` template file as a
+``renderer``. We share the same template between add and edit views, thus
``edit.pt`` instead of ``add.pt``.
The ``add_page`` function will be invoked when a user clicks on a WikiWord
@@ -181,10 +242,10 @@ Page resource).
The request :term:`subpath` in :app:`Pyramid` is the sequence of names that
are found *after* the :term:`view name` in the URL segments given in the
``PATH_INFO`` of the WSGI request as the result of :term:`traversal`. If our
-add view is invoked via, e.g. ``http://localhost:6543/add_page/SomeName``,
+add view is invoked via, e.g., ``http://localhost:6543/add_page/SomeName``,
the :term:`subpath` will be a tuple: ``('SomeName',)``.
-The add view takes the zeroth element of the subpath (the wiki page name),
+The add view takes the zero\ :sup:`th` element of the subpath (the wiki page name),
and aliases it to the name attribute in order to know the name of the page
we're trying to add.
@@ -198,7 +259,7 @@ order to satisfy the edit form's desire to have *some* page object exposed as
``page``, and we'll render the template to a response.
If the view rendering *is* a result of a form submission (if the expression
-``'form.submitted' in request.params`` is ``True``), we scrape the page body
+``'form.submitted' in request.params`` is ``True``), we grab the page body
from the form data, create a Page object using the name in the subpath and
the page body, and save it into "our context" (the Wiki) using the
``__setitem__`` method of the context. We then redirect back to the
@@ -207,15 +268,15 @@ the page body, and save it into "our context" (the Wiki) using the
The ``edit_page`` view function
-------------------------------
-Here is the code for the ``edit_page`` view function and its decorator, which
-will be added to ``views.py``:
+Here is the code for the ``edit_page`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views.py
- :lines: 52-60
- :language: python
+ :lines: 52-60
+ :lineno-match:
+ :language: python
-The ``edit_page`` function will be configured to respond when the context is
-a Page resource and the :term:`view name` is ``edit_page``. We'll provide it
+The ``edit_page`` function is configured to respond when the context is
+a Page resource and the :term:`view name` is ``edit_page``. We provide it
with a ``@view_config`` decorator which names the string ``edit_page`` as its
:term:`view name` (via ``name=``), the class ``tutorial.models.Page`` as its
context, and the renderer named ``templates/edit.pt``. This means that when
@@ -240,114 +301,95 @@ If the view execution *is* a result of a form submission (if the expression
attribute of the page context. It then redirects to the default view of the
context (the page), which will always be the ``view_page`` view.
-Viewing the Result of all Our Edits to ``views.py``
-===================================================
-
-The result of all of our edits to ``views.py`` will leave it looking like
-this:
-
-.. literalinclude:: src/views/tutorial/views.py
- :linenos:
- :language: python
-
-Adding Templates
+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:`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.
-The ``view.pt`` Template
+The ``view.pt`` template
------------------------
-Create ``tutorial/tutorial/templates/view.pt`` and add the following
-content:
+Rename ``tutorial/templates/mytemplate.pt`` to ``tutorial/templates/view.pt`` and edit the emphasized lines to look like the following:
.. literalinclude:: src/views/tutorial/templates/view.pt
- :linenos:
- :language: xml
+ :linenos:
+ :language: html
+ :emphasize-lines: 11-12,37-52
This template is used by ``view_page()`` for displaying a single
wiki page. It includes:
-- A ``div`` element that is replaced with the ``content``
- value provided by the view (rows 45-47). ``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 (rows 49-51).
+- A ``div`` element that is replaced with the ``content`` value provided by
+ the view (lines 37-39). ``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 41-43).
-The ``edit.pt`` Template
+The ``edit.pt`` template
------------------------
-Create ``tutorial/tutorial/templates/edit.pt`` and add the following
-content:
+Copy ``tutorial/templates/view.pt`` to ``tutorial/templates/edit.pt`` and edit the emphasized lines to look like the following:
.. literalinclude:: src/views/tutorial/templates/edit.pt
- :linenos:
- :language: xml
-
-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:
-
-- A 10 row by 60 column ``textarea`` field named ``body`` that is filled
- with any existing page data when it is rendered (rows 46-47).
-- A submit button that has the name ``form.submitted`` (row 48).
-
-The form POSTs back to the "save_url" argument supplied
-by the view (row 45). The view will use the ``body`` and
-``form.submitted`` values.
-
-.. note:: Our templates use a ``request`` object that
- none of our tutorial views return in their dictionary.
- ``request`` is one of several
- names that are available "by default" in a template when a template
- renderer is used. See :ref:`renderer_system_values` for
- information about other names that are available by default
- when a template is used as a renderer.
-
-Static Assets
+ :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:
+
+- A 10-row by 60-column ``textarea`` field named ``body`` that is filled
+ with any existing page data when it is rendered (line 46).
+- A submit button that has the name ``form.submitted`` (line 49).
+
+The form POSTs back to the ``save_url`` argument supplied by the view (line
+44). The view will use the ``body`` and ``form.submitted`` values.
+
+.. note:: Our templates use a ``request`` object that none of our tutorial
+ views return in their dictionary. ``request`` is one of several names that
+ are available "by default" in a template when a template renderer is used.
+ See :ref:`renderer_system_values` for information about other names that
+ are available by default when a template is used as a renderer.
+
+
+Static assets
-------------
-Our templates name a single static asset named ``pylons.css``. We don't need
-to create this file within our package's ``static`` directory because it was
-provided at the time we created the project. This file is a little too long to
-replicate within the body of this guide, however it is available `online
-<https://github.com/Pylons/pyramid/blob/master/docs/tutorials/wiki/src/views/tutorial/static/pylons.css>`_.
+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.
-This CSS file will be accessed via
-e.g. ``/static/pylons.css`` by virtue of the call to
+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.
+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
+Viewing the application in a browser
====================================
We can finally examine our application in a browser (See
:ref:`wiki-start-the-application`). Launch a browser and visit
-each of the following URLs, check that the result is as expected:
+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 resource.
+- http://localhost:6543/ invokes the ``view_wiki`` view. This always
+ redirects to the ``view_page`` view of the ``FrontPage`` Page resource.
-- ``http://localhost:6543/FrontPage/`` invokes
- the ``view_page`` view of the front page resource. This is
- because it's the :term:`default view` (a view without a ``name``) for Page
- resources.
+- http://localhost:6543/FrontPage/ invokes the ``view_page`` view of the front
+ page resource. This is because it's the :term:`default view` (a view
+ without a ``name``) for Page resources.
-- ``http://localhost:6543/FrontPage/edit_page``
- invokes the edit view for the ``FrontPage`` Page resource.
+- http://localhost:6543/FrontPage/edit_page invokes the edit view for the
+ ``FrontPage`` Page resource.
-- ``http://localhost:6543/add_page/SomePageName``
- invokes the add view for a Page.
+- http://localhost:6543/add_page/SomePageName invokes the add view for a Page.
-- To generate an error, visit ``http://localhost:6543/add_page`` which
- will generate an ``IndexErrorr: tuple index out of range`` error.
- You'll see an interactive traceback
- facility provided by :term:`pyramid_debugtoolbar`.
+- To generate an error, visit http://localhost:6543/add_page which will
+ generate an ``IndexError: tuple index out of range`` error. You'll see an
+ interactive traceback facility provided by :term:`pyramid_debugtoolbar`.
diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst
index eb785dd1c..30d443bb8 100644
--- a/docs/tutorials/wiki/design.rst
+++ b/docs/tutorials/wiki/design.rst
@@ -1,23 +1,25 @@
-==========
+.. _wiki_design:
+
+======
Design
-==========
+======
-Following is a quick overview of our wiki application, to help
-us understand the changes that we will be doing next in our
-default files generated by the ``zodb`` scaffold.
+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 ``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 ``setup.py`` file.
+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
+``setup.py`` file.
Models
------
-The root resource, named *Wiki*, will be a mapping of wiki page
+The root resource named ``Wiki`` will be a mapping of wiki page
names to page resources. The page resources will be instances
of a *Page* class and they store the text content.
@@ -29,9 +31,9 @@ To add a page to the wiki, a new instance of the page resource
is created and its name and reference are added to the Wiki
mapping.
-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.
+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
-----
@@ -41,11 +43,8 @@ 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:`Chameleon`. Chameleon is a variant of :term:`ZPT`, which is an XML-based templating language.
+
Security
--------
@@ -53,18 +52,17 @@ Security
We'll eventually be adding security to our application. The components we'll
use to do this are below.
-- USERS, a dictionary mapping usernames to their
+- USERS, a dictionary mapping :term:`userids <userid>` to their
corresponding passwords.
-- GROUPS, a dictionary mapping usernames to a
- list of groups to which they belong to.
+- 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.
+- ``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`:
+- An :term:`ACL` is attached to the root :term:`resource`. Each row below
+ details an :term:`ACE`:
+----------+----------------+----------------+
| Action | Principal | Permission |
@@ -125,7 +123,7 @@ listed in the following table:
| | | | authenticate. | | |
| | | | | | |
| | | | - If authentication | | |
-| | | | successful, | | |
+| | | | succeeds, | | |
| | | | redirect to the | | |
| | | | page that we | | |
| | | | came from. | | |
@@ -145,6 +143,6 @@ listed in the following table:
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
+.. [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/wiki/distributing.rst b/docs/tutorials/wiki/distributing.rst
index 9c63cf0bd..36d00adb4 100644
--- a/docs/tutorials/wiki/distributing.rst
+++ b/docs/tutorials/wiki/distributing.rst
@@ -1,42 +1,40 @@
+.. _wiki_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.
+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 contains the ``tutorial`` package and the
+``setup.py`` file.
-On UNIX:
+On Unix:
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/python setup.py sdist
+ $VENV/bin/python setup.py sdist
On Windows:
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut> %VENV%\Scripts\python setup.py sdist
+ %VENV%\Scripts\python setup.py sdist
The output of such a command will be something like:
.. code-block:: text
- running sdist
- # .. more output ..
- creating dist
- tar -cf dist/tutorial-0.1.tar tutorial-0.1
- gzip -f9 dist/tutorial-0.1.tar
- removing 'tutorial-0.1' (and everything under it)
-
-Note that this command creates a tarball in the "dist" subdirectory
-named ``tutorial-0.1.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 <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 download from
-PyPI.
-
+ running sdist
+ # more output
+ creating dist
+ 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
+``pip install`` command directly at it. Or you can upload it to `PyPI
+<https://pypi.org/>`_ and share it with the rest of the world, where
+it 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 981d135c7..7bd58656b 100644
--- a/docs/tutorials/wiki/index.rst
+++ b/docs/tutorials/wiki/index.rst
@@ -3,15 +3,14 @@
ZODB + Traversal Wiki Tutorial
==============================
-This tutorial introduces a :term:`traversal` -based :app:`Pyramid`
-application to a developer familiar with Python. It will be most familiar to
-developers with previous :term:`Zope` experience. When we're done with the
-tutorial, the developer will have created a basic Wiki application with
+This tutorial introduces a :term:`ZODB` and :term:`traversal`-based
+:app:`Pyramid` application to a developer familiar with Python. It will be
+most familiar to developers with previous :term:`Zope` experience. When
+finished, the developer will have created a basic Wiki application with
authentication.
For cut and paste purposes, the source code for all stages of this
-tutorial can be browsed on GitHub at `docs/tutorials/wiki/src
-<https://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src>`_,
+tutorial can be browsed on GitHub at `GitHub <https://github.com/Pylons/pyramid/>`_ for a specific branch or version under ``docs/tutorials/wiki/src``,
which corresponds to the same location if you have Pyramid sources.
.. toctree::
@@ -26,4 +25,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 b51254b92..d0037e584 100644
--- a/docs/tutorials/wiki/installation.rst
+++ b/docs/tutorials/wiki/installation.rst
@@ -1,199 +1,397 @@
+.. _wiki_installation:
+
============
Installation
============
-Preparation
-===========
+Before you begin
+----------------
-Follow the steps in :ref:`installing_chapter`, but name the virtualenv
-directory ``pyramidtut``.
+This tutorial assumes that you have already followed the steps in
+:ref:`installing_chapter`, except **do not create a virtual environment or
+install Pyramid**. Thereby you will satisfy the following requirements.
-Preparation, UNIX
------------------
+* A Python interpreter is installed on your operating system.
+* You've satisfied the :ref:`requirements-for-installing-packages`.
-#. Switch to the ``pyramidtut`` directory:
+Install cookiecutter
+--------------------
+We will use a :term:`cookiecutter` to create a Python package project from a Python package project template. See `Cookiecutter Installation <https://cookiecutter.readthedocs.io/en/latest/installation.html>`_ for instructions.
- .. code-block:: text
- $ cd pyramidtut
+Generate a Pyramid project from a cookiecutter
+----------------------------------------------
-#. Install tutorial dependencies:
+We will create a Pyramid project in your home directory for Unix or at the root for Windows. It is assumed you know the path to where you installed ``cookiecutter``. Issue the following commands and override the defaults in the prompts as follows.
- .. code-block:: text
+On Unix
+^^^^^^^
- $ $VENV/bin/easy_install docutils pyramid_tm pyramid_zodbconn \
- pyramid_debugtoolbar nose coverage
+.. code-block:: bash
-Preparation, Windows
---------------------
+ cd ~
+ cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout master
+On Windows
+^^^^^^^^^^
-#. Switch to the ``pyramidtut`` directory:
+.. code-block:: doscon
- .. code-block:: text
+ cd \
+ cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout master
- c:\> cd pyramidtut
+On all operating systems
+^^^^^^^^^^^^^^^^^^^^^^^^
+If prompted for the first item, accept the default ``yes`` by hitting return.
-#. Install tutorial dependencies:
+.. code-block:: text
- .. code-block:: text
+ You've cloned ~/.cookiecutters/pyramid-cookiecutter-theone before.
+ Is it okay to delete and re-clone it? [yes]: yes
+ project_name [Pyramid Scaffold]: myproj
+ repo_name [myproj]: tutorial
+ Select template_language:
+ 1 - jinja2
+ 2 - chameleon
+ 3 - mako
+ Choose from 1, 2, 3 [1]: 1
+ Select backend:
+ 1 - none
+ 2 - sqlalchemy
+ 3 - zodb
+ Choose from 1, 2, 3 [1]: 3
- c:\pyramidtut> %VENV%\Scripts\easy_install docutils pyramid_tm \
- pyramid_zodbconn pyramid_debugtoolbar nose coverage
+Change directory into your newly created project
+------------------------------------------------
-.. _making_a_project:
+On Unix
+^^^^^^^
-Make a Project
-==============
+.. code-block:: bash
-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.
+ cd tutorial
-The below instructions assume your current working directory is the
-"virtualenv" named "pyramidtut".
+On Windows
+^^^^^^^^^^
-On UNIX:
+.. code-block:: doscon
-.. code-block:: text
+ cd tutorial
- $ $VENV/bin/pcreate -s zodb tutorial
-On Windows:
+Set and use a ``VENV`` environment variable
+-------------------------------------------
-.. code-block:: text
+We will set the ``VENV`` environment variable to the absolute path of the virtual environment, and use it going forward.
- c:\pyramidtut> %VENV%\Scripts\pcreate -s zodb tutorial
+On Unix
+^^^^^^^
-.. note:: You don't have to call it `tutorial` -- the code uses
- relative paths for imports and finding templates and static
- resources.
+.. code-block:: bash
-.. note:: If you are using Windows, the ``zodb`` scaffold
- doesn't currently 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.
+ export VENV=~/tutorial
-Install the Project in "Development Mode"
-=========================================
+On Windows
+^^^^^^^^^^
-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 virtualenv Python interpreter.
+.. code-block:: doscon
-On UNIX:
+ set VENV=c:\tutorial
-.. code-block:: text
- $ cd tutorial
- $ $VENV/bin/python setup.py develop
+Create a virtual environment
+----------------------------
-On Windows:
+On Unix
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
+
+ python3 -m venv $VENV
+
+On Windows
+^^^^^^^^^^
+
+Each version of Python uses different paths, so you might need to adjust the path to the command for your Python version. Recent versions of the Python 3 installer for Windows now install a Python launcher.
+
+Python 2.7:
+
+.. code-block:: doscon
+
+ c:\Python27\Scripts\virtualenv %VENV%
+
+Python 3.7:
+
+.. code-block:: doscon
+
+ python -m venv %VENV%
+
+
+Upgrade packaging tools in the virtual environment
+--------------------------------------------------
+
+On Unix
+^^^^^^^
+
+.. code-block:: bash
+
+ $VENV/bin/pip install --upgrade pip setuptools
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ %VENV%\Scripts\pip install --upgrade pip setuptools
+
+
+.. _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. We will install testing requirements at the same time. We do so with the following command.
+
+On Unix
+^^^^^^^
+
+.. code-block:: bash
+
+ $VENV/bin/pip install -e ".[testing]"
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ %VENV%\Scripts\pip install -e ".[testing]"
+
+On all operating systems
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+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 BTrees-4.3.1 Chameleon-3.0 Mako-1.0.6 \
+ MarkupSafe-0.23 PasteDeploy-1.5.2 Pygments-2.1.3 WebOb-1.6.3 \
+ WebTest-2.0.23 ZConfig-3.1.0 ZEO-5.0.4 ZODB-5.1.1 ZODB3-3.11.0 \
+ beautifulsoup4-4.5.1 coverage-4.2 mock-2.0.0 pbr-1.10.0 persistent-4.2.2 \
+ py-1.4.31 pyramid-1.7.3 pyramid-chameleon-0.3 pyramid-debugtoolbar-3.0.5 \
+ pyramid-mako-1.0.2 pyramid-tm-1.1.1 pyramid-zodbconn-0.7 pytest-3.0.5 \
+ pytest-cov-2.4.0 repoze.lru-0.6 six-1.10.0 transaction-2.0.3 \
+ translationstring-1.3 tutorial venusian-1.0 waitress-1.0.1 \
+ zc.lockfile-1.2.1 zdaemon-4.2.0 zodbpickle-0.6.0 zodburi-2.0 \
+ zope.deprecation-4.2.0 zope.interface-4.3.3
+
+Testing requirements are defined in our project's ``setup.py`` file, in the ``tests_require`` and ``extras_require`` stanzas.
+
+.. literalinclude:: src/installation/setup.py
+ :language: python
+ :lineno-match:
+ :lines: 24-28
+
+.. literalinclude:: src/installation/setup.py
+ :language: python
+ :lineno-match:
+ :lines: 48-50
- C:\pyramidtut> cd tutorial
- C:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
.. _running_tests:
-Run the 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. The following commands
+provide options to ``pytest`` that specify the module for which its tests shall be
+run, and to run ``pytest`` in quiet mode.
-On UNIX:
+On Unix
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/python setup.py test -q
+ $VENV/bin/pytest -q
-On Windows:
+On Windows
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
+
+ %VENV%\Scripts\pytest -q
+
+For a successful test run, you should see output that ends like this:
+
+.. code-block:: bash
+
+ .
+ 1 passed in 0.24 seconds
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
-Expose Test Coverage Information
-================================
+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 ``pytest`` command to see test coverage information. This
+runs the tests in the same way that ``pytest`` does, but provides additional
+:term:`coverage` information, exposing which lines of your project are covered by the
tests.
-On UNIX:
+We've already installed the ``pytest-cov`` package into our virtual
+environment, so we can run the tests with coverage.
-.. code-block:: text
+On Unix
+^^^^^^^
- $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage
+.. code-block:: bash
-On Windows:
+ $VENV/bin/pytest --cov --cov-report=term-missing
-.. code-block:: text
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ %VENV%\Scripts\pytest --cov --cov-report=term-missing
+
+If successful, you will see output something like this:
+
+.. code-block:: bash
+
+ ======================== test session starts ========================
+ platform Python 3.6.0, pytest-3.0.5, py-1.4.31, pluggy-0.4.0
+ rootdir: /Users/stevepiercy/tutorial, inifile:
+ plugins: cov-2.4.0
+ collected 1 items
+
+ tutorial/tests.py .
+ ------------------ coverage: platform Python 3.6.0 ------------------
+ Name Stmts Miss Cover Missing
+ -------------------------------------------------------
+ tutorial/__init__.py 14 9 36% 7-8, 14-20
+ tutorial/models.py 10 6 40% 9-14
+ tutorial/views.py 4 0 100%
+ -------------------------------------------------------
+ TOTAL 28 15 46%
+
+ ===================== 1 passed in 0.31 seconds ======================
+
+Our package doesn't quite have 100% test coverage.
+
+
+.. _test_and_coverage_cookiecutter_defaults_zodb:
+
+Test and coverage cookiecutter defaults
+---------------------------------------
+
+The Pyramid cookiecutter includes configuration defaults for ``pytest`` and
+test coverage. These configuration files are ``pytest.ini`` and
+``.coveragerc``, located at the root of your package. Without these defaults,
+we would need to specify the path to the module on which we want to run tests
+and coverage.
+
+On Unix
+^^^^^^^
- c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial ^
- --cover-erase --with-coverage
+.. code-block:: bash
+
+ $VENV/bin/pytest --cov=tutorial tutorial/tests.py -q
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ %VENV%\Scripts\pytest --cov=tutorial tutorial\tests.py -q
+
+``pytest`` follows :ref:`conventions for Python test discovery
+<pytest:test discovery>`, and the configuration defaults from the cookiecutter
+tell ``pytest`` where to find the module on which we want to run tests and
+coverage.
+
+.. seealso:: See ``pytest``'s documentation for :ref:`pytest:usage` or invoke
+ ``pytest -h`` to see its full set of options.
-Looks like the code in the ``zodb`` scaffold for ZODB projects is
-missing some test coverage, particularly in the file named
-``models.py``.
.. _wiki-start-the-application:
-Start the Application
-=====================
+Start the application
+---------------------
-Start the application.
+Start the application. See :ref:`what_is_this_pserve_thing` for more
+information on ``pserve``.
-On UNIX:
+On Unix
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/pserve development.ini --reload
+ $VENV/bin/pserve development.ini --reload
-On Windows:
+On Windows
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload
+ %VENV%\Scripts\pserve development.ini --reload
.. note::
Your OS firewall, if any, may pop up a dialog asking for authorization
to allow python to accept incoming network connections.
-Visit the Application in a Browser
-==================================
+If successful, you will see something like this on your console:
+
+.. code-block:: text
+
+ Starting subprocess with file monitor
+ Starting server in PID 44078.
+ Serving on http://localhost:6543
+ Serving on http://localhost:6543
+
+This means the server is ready to accept requests.
+
-In a browser, visit `http://localhost:6543/ <http://localhost:6543>`_. You
-will see the generated application's default page.
+Visit the application in a browser
+----------------------------------
+
+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:
+Decisions the cookiecutter backend option ``zodb`` has made for you
+-------------------------------------------------------------------
+
+When creating a project and selecting the backend option of ``zodb``, the cookiecutter makes the following assumptions:
-- you are willing to use :term:`ZODB` as persistent storage
+- You are willing to use :term:`ZODB` for 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.
+
+- You want to use pyramid_zodbconn_, pyramid_tm_, and the transaction_ packages
+ to manage connections and transactions with :term:`ZODB`.
.. note::
- :app:`Pyramid` supports any persistent storage mechanism (e.g., a SQL
- database or filesystem files). :app:`Pyramid` also supports an additional
- mechanism to map URLs to code (:term:`URL dispatch`). However, for the
- purposes of this tutorial, we'll only be using traversal and ZODB.
+ :app:`Pyramid` supports any persistent storage mechanism (e.g., an SQL
+ database or filesystem files). It also supports an additional mechanism to
+ map URLs to code (:term:`URL dispatch`). However, for the purposes of this
+ tutorial, we'll only be using :term:`traversal` and :term:`ZODB`.
+
+.. _pyramid_chameleon:
+ https://docs.pylonsproject.org/projects/pyramid-chameleon/en/latest/
+
+.. _pyramid_tm:
+ https://docs.pylonsproject.org/projects/pyramid-tm/en/latest/
+
+.. _pyramid_zodbconn:
+ https://docs.pylonsproject.org/projects/pyramid-zodbconn/en/latest/
+
+.. _transaction:
+ https://zodb.readthedocs.io/en/latest/transactions.html
diff --git a/docs/tutorials/wiki/src/authorization/.coveragerc b/docs/tutorials/wiki/src/authorization/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki/src/authorization/.gitignore b/docs/tutorials/wiki/src/authorization/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki/src/authorization/CHANGES.txt b/docs/tutorials/wiki/src/authorization/CHANGES.txt
index e14f633ab..14b902fd1 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..8a56d14af 100644
--- a/docs/tutorials/wiki/src/authorization/README.txt
+++ b/docs/tutorials/wiki/src/authorization/README.txt
@@ -1,4 +1,29 @@
-tutorial README
+myproj
+======
+Getting Started
+---------------
+- Change directory into your newly created project.
+ cd tutorial
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/authorization/development.ini b/docs/tutorials/wiki/src/authorization/development.ini
index 72bd22e54..228f18f36 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,28 +13,29 @@ 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
+retry.attempts = 3
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -62,4 +63,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..46b1e331b 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -11,25 +11,25 @@ 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
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/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/pytest.ini b/docs/tutorials/wiki/src/authorization/pytest.ini
new file mode 100644
index 000000000..8b76bc410
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = *.py
diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py
index 5ab4f73cd..7011387f6 100644
--- a/docs/tutorials/wiki/src/authorization/setup.py
+++ b/docs/tutorials/wiki/src/authorization/setup.py
@@ -9,39 +9,51 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9a',
'pyramid_chameleon',
+ 'pyramid_debugtoolbar',
+ 'pyramid_retry',
+ 'pyramid_tm',
'pyramid_zodbconn',
'transaction',
- 'pyramid_tm',
- 'pyramid_debugtoolbar',
'ZODB3',
'waitress',
'docutils',
- ]
+ 'bcrypt',
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ '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",
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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',
],
- author='',
- author_email='',
- url='',
- keywords='web pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- 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/__init__.py b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py
index 39b94abd1..58635ea74 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py
@@ -15,13 +15,18 @@ def root_factory(request):
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
authn_policy = AuthTktAuthenticationPolicy(
'sosecret', callback=groupfinder, hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()
- config = Configurator(root_factory=root_factory, settings=settings)
- 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()
+ with Configurator(settings=settings) as config:
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(authz_policy)
+ config.include('pyramid_chameleon')
+ config.include('pyramid_tm')
+ config.include('pyramid_retry')
+ config.include('pyramid_zodbconn')
+ config.set_root_factory(root_factory)
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/models.py b/docs/tutorials/wiki/src/authorization/tutorial/models.py
index 582ff0d7e..ebd70e912 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/models.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/models.py
@@ -17,13 +17,11 @@ 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
frontpage.__name__ = 'FrontPage'
frontpage.__parent__ = app_root
zodb_root['app_root'] = app_root
- import transaction
- transaction.commit()
return zodb_root['app_root']
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/pshell.py b/docs/tutorials/wiki/src/authorization/tutorial/pshell.py
new file mode 100644
index 000000000..3d026291b
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/tutorial/pshell.py
@@ -0,0 +1,11 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['models'] = models
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/security.py b/docs/tutorials/wiki/src/authorization/tutorial/security.py
index d88c9c71f..cbb3acd5d 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/security.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/security.py
@@ -1,5 +1,18 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
+import bcrypt
+
+
+def hash_password(pw):
+ hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
+ # return unicode instead of bytes because databases handle it better
+ return hashed_pw.decode('utf-8')
+
+def check_password(expected_hash, pw):
+ if expected_hash is not None:
+ return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8'))
+ return False
+
+USERS = {'editor': hash_password('editor'),
+ 'viewer': hash_password('viewer')}
GROUPS = {'editor':['group:editors']}
def groupfinder(userid, request):
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/authorization/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/authorization/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/authorization/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/ie6.css b/docs/tutorials/wiki/src/authorization/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/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/tutorials/wiki/src/authorization/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/authorization/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/pylons.css b/docs/tutorials/wiki/src/authorization/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/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/tutorials/wiki/src/authorization/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/theme.css b/docs/tutorials/wiki/src/authorization/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/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/wiki/src/authorization/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/authorization/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt
index c3a0acf6b..eedb83da4 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt
+++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt
@@ -1,58 +1,72 @@
-<!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>${page.__name__} - 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="/static/favicon.ico" />
- <link rel="stylesheet"
- href="/static/pylons.css"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="/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="/static/pyramid-small.png" />
+<!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 rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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>
- </div>
- <div id="middle">
- <div class="middle align-right">
- <div id="left" class="app-welcome align-left">
- Editing <b><span tal:replace="page.__name__">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</div>
</div>
</div>
- <div id="bottom">
- <div class="bottom">
- <form action="${save_url}" method="post">
- <textarea name="body" tal:content="page.data" rows="10"
- cols="60"/><br/>
- <input type="submit" name="form.submitted" value="Save"/>
- </form>
- </div>
- </div>
- </div>
-</body>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt
index 3612dccde..626db6637 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt
+++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt
@@ -1,54 +1,75 @@
-<!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="/static/favicon.ico" />
- <link rel="stylesheet"
- href="/static/pylons.css"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="/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="/static/pyramid-small.png" />
+<!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 rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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>
- </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 class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</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>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt
deleted file mode 100644
index 13b41f823..000000000
--- a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,73 +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>The Pyramid Web Framework</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="/static/favicon.ico" />
- <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <!--[if lte IE 6]>
- <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" />
- <![endif]-->
-</head>
-<body>
- <div id="wrap">
- <div id="top">
- <div class="top align-center">
- <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
- </div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org/">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt
index 90e20764d..f2a9249ef 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt
@@ -1,61 +1,72 @@
-<!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>${page.__name__} - Pyramid tutorial wiki (based on
+<!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>
- <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="/static/favicon.ico" />
- <link rel="stylesheet"
- href="/static/pylons.css"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="/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="/static/pyramid-small.png" />
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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>
+ <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>
+ </div>
+ </div>
</div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-right">
- <div id="left" class="app-welcome align-left">
- Viewing <b><span tal:replace="page.__name__">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div tal:replace="structure content">
- Page text goes here.
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
</div>
</div>
- </div>
-</body>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/tests.py b/docs/tutorials/wiki/src/authorization/tutorial/tests.py
index 0b9046d47..ca7a47279 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'], 'myproj')
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py
index 62e96e0e7..ea2da01af 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/views.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py
@@ -14,7 +14,7 @@ from pyramid.security import (
)
-from .security import USERS
+from .security import USERS, check_password
from .models import Page
# regular expression used to find WikiWords
@@ -37,15 +37,14 @@ 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,12 +57,11 @@ 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
page.__parent__ = context
-
return dict(page=page, save_url=save_url,
logged_in=request.authenticated_userid)
@@ -73,7 +71,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 +84,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 = ''
@@ -94,22 +92,23 @@ def login(request):
if 'form.submitted' in request.params:
login = request.params['login']
password = request.params['password']
- if USERS.get(login) == password:
+ if check_password(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/.coveragerc b/docs/tutorials/wiki/src/basiclayout/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki/src/basiclayout/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki/src/basiclayout/.gitignore b/docs/tutorials/wiki/src/basiclayout/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki/src/basiclayout/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki/src/basiclayout/CHANGES.txt b/docs/tutorials/wiki/src/basiclayout/CHANGES.txt
index 35a34f332..14b902fd1 100644
--- a/docs/tutorials/wiki/src/basiclayout/CHANGES.txt
+++ b/docs/tutorials/wiki/src/basiclayout/CHANGES.txt
@@ -1,4 +1,4 @@
0.0
---
-- Initial version
+- Initial version.
diff --git a/docs/tutorials/wiki/src/basiclayout/README.txt b/docs/tutorials/wiki/src/basiclayout/README.txt
index d41f7f90f..8a56d14af 100644
--- a/docs/tutorials/wiki/src/basiclayout/README.txt
+++ b/docs/tutorials/wiki/src/basiclayout/README.txt
@@ -1,4 +1,29 @@
-tutorial README
+myproj
+======
+Getting Started
+---------------
+- Change directory into your newly created project.
+ cd tutorial
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/basiclayout/development.ini b/docs/tutorials/wiki/src/basiclayout/development.ini
index 72bd22e54..228f18f36 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,28 +13,29 @@ 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
+retry.attempts = 3
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -62,4 +63,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..46b1e331b 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -11,25 +11,25 @@ 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
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/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/pytest.ini b/docs/tutorials/wiki/src/basiclayout/pytest.ini
new file mode 100644
index 000000000..8b76bc410
--- /dev/null
+++ b/docs/tutorials/wiki/src/basiclayout/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = *.py
diff --git a/docs/tutorials/wiki/src/basiclayout/setup.py b/docs/tutorials/wiki/src/basiclayout/setup.py
index da79881ab..e05e279e2 100644
--- a/docs/tutorials/wiki/src/basiclayout/setup.py
+++ b/docs/tutorials/wiki/src/basiclayout/setup.py
@@ -9,38 +9,49 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9a',
'pyramid_chameleon',
+ 'pyramid_debugtoolbar',
+ 'pyramid_retry',
+ 'pyramid_tm',
'pyramid_zodbconn',
'transaction',
- 'pyramid_tm',
- 'pyramid_debugtoolbar',
'ZODB3',
'waitress',
- ]
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ '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",
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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',
],
- author='',
- author_email='',
- url='',
- keywords='web pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- 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/__init__.py b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py
index f2a86df47..f2b3c9568 100644
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py
@@ -11,8 +11,13 @@ def root_factory(request):
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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+ with Configurator(settings=settings) as config:
+ config.include('pyramid_chameleon')
+ config.include('pyramid_tm')
+ config.include('pyramid_retry')
+ config.include('pyramid_zodbconn')
+ config.set_root_factory(root_factory)
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki/src/basiclayout/tutorial/models.py
index a94b36ef4..aca6a4129 100644
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/models.py
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/models.py
@@ -6,9 +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
- transaction.commit()
return zodb_root['app_root']
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/pshell.py b/docs/tutorials/wiki/src/basiclayout/tutorial/pshell.py
new file mode 100644
index 000000000..3d026291b
--- /dev/null
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/pshell.py
@@ -0,0 +1,11 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['models'] = models
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/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/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/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/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki/src/basiclayout/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/wiki/src/basiclayout/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt
index 13b41f823..d63ea8c45 100644
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt
@@ -1,73 +1,65 @@
-<!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>The Pyramid Web Framework</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="/static/favicon.ico" />
- <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <!--[if lte IE 6]>
- <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" />
- <![endif]-->
-</head>
-<body>
- <div id="wrap">
- <div id="top">
- <div class="top align-center">
- <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
+<!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>Cookiecutter ZODB project for the Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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">ZODB Project</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
+ </div>
+ </div>
</div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org/">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
+ <div class="row">
+ <div class="links">
+ <ul>
+ <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="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
</ul>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</div>
</div>
</div>
- </div>
-</body>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py
index 7f6523c66..ca7a47279 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()
@@ -13,4 +14,4 @@ class ViewTests(unittest.TestCase):
from .views import my_view
request = testing.DummyRequest()
info = my_view(request)
- self.assertEqual(info['project'], 'tutorial')
+ self.assertEqual(info['project'], 'myproj')
diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki/src/basiclayout/tutorial/views.py
index 628ce15ed..c1878bdd0 100644
--- a/docs/tutorials/wiki/src/basiclayout/tutorial/views.py
+++ b/docs/tutorials/wiki/src/basiclayout/tutorial/views.py
@@ -4,4 +4,4 @@ from .models import MyModel
@view_config(context=MyModel, renderer='templates/mytemplate.pt')
def my_view(request):
- return {'project': 'tutorial'}
+ return {'project': 'myproj'}
diff --git a/docs/tutorials/wiki/src/installation/.coveragerc b/docs/tutorials/wiki/src/installation/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki/src/installation/.gitignore b/docs/tutorials/wiki/src/installation/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki/src/installation/CHANGES.txt b/docs/tutorials/wiki/src/installation/CHANGES.txt
new file mode 100644
index 000000000..14b902fd1
--- /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..8a56d14af
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/README.txt
@@ -0,0 +1,29 @@
+myproj
+======
+
+Getting Started
+---------------
+
+- Change directory into your newly created project.
+
+ cd tutorial
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/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..228f18f36
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/development.ini
@@ -0,0 +1,66 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+
+zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+retry.attempts = 3
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/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..46b1e331b
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/production.ini
@@ -0,0 +1,60 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = *:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/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/pytest.ini b/docs/tutorials/wiki/src/installation/pytest.ini
new file mode 100644
index 000000000..8b76bc410
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = *.py
diff --git a/docs/tutorials/wiki/src/installation/setup.py b/docs/tutorials/wiki/src/installation/setup.py
new file mode 100644
index 000000000..e05e279e2
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/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 = [
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9a',
+ 'pyramid_chameleon',
+ 'pyramid_debugtoolbar',
+ 'pyramid_retry',
+ 'pyramid_tm',
+ 'pyramid_zodbconn',
+ 'transaction',
+ 'ZODB3',
+ 'waitress',
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ 'pytest-cov',
+]
+
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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..f2b3c9568
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/__init__.py
@@ -0,0 +1,23 @@
+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.
+ """
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+ with Configurator(settings=settings) as config:
+ config.include('pyramid_chameleon')
+ config.include('pyramid_tm')
+ config.include('pyramid_retry')
+ config.include('pyramid_zodbconn')
+ config.set_root_factory(root_factory)
+ 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..aca6a4129
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/models.py
@@ -0,0 +1,12 @@
+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
+ return zodb_root['app_root']
diff --git a/docs/tutorials/wiki/src/installation/tutorial/pshell.py b/docs/tutorials/wiki/src/installation/tutorial/pshell.py
new file mode 100644
index 000000000..3d026291b
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/pshell.py
@@ -0,0 +1,11 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['models'] = models
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/wiki/src/installation/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/installation/tutorial/templates/mytemplate.pt
new file mode 100644
index 000000000..d63ea8c45
--- /dev/null
+++ b/docs/tutorials/wiki/src/installation/tutorial/templates/mytemplate.pt
@@ -0,0 +1,65 @@
+<!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>Cookiecutter ZODB project for the Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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">ZODB Project</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="links">
+ <ul>
+ <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="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://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="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
+</html>
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..ca7a47279
--- /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'], 'myproj')
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..c1878bdd0
--- /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': 'myproj'}
diff --git a/docs/tutorials/wiki/src/models/.coveragerc b/docs/tutorials/wiki/src/models/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki/src/models/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki/src/models/.gitignore b/docs/tutorials/wiki/src/models/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki/src/models/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki/src/models/CHANGES.txt b/docs/tutorials/wiki/src/models/CHANGES.txt
index ffa255da8..14b902fd1 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..8a56d14af 100644
--- a/docs/tutorials/wiki/src/models/README.txt
+++ b/docs/tutorials/wiki/src/models/README.txt
@@ -1,4 +1,29 @@
-tutorial README
+myproj
+======
+Getting Started
+---------------
+- Change directory into your newly created project.
+ cd tutorial
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/models/development.ini b/docs/tutorials/wiki/src/models/development.ini
index 72bd22e54..228f18f36 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,28 +13,29 @@ 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
+retry.attempts = 3
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -62,4 +63,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..46b1e331b 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -11,25 +11,25 @@ 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
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/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/pytest.ini b/docs/tutorials/wiki/src/models/pytest.ini
new file mode 100644
index 000000000..8b76bc410
--- /dev/null
+++ b/docs/tutorials/wiki/src/models/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = *.py
diff --git a/docs/tutorials/wiki/src/models/setup.py b/docs/tutorials/wiki/src/models/setup.py
index da79881ab..e05e279e2 100644
--- a/docs/tutorials/wiki/src/models/setup.py
+++ b/docs/tutorials/wiki/src/models/setup.py
@@ -9,38 +9,49 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9a',
'pyramid_chameleon',
+ 'pyramid_debugtoolbar',
+ 'pyramid_retry',
+ 'pyramid_tm',
'pyramid_zodbconn',
'transaction',
- 'pyramid_tm',
- 'pyramid_debugtoolbar',
'ZODB3',
'waitress',
- ]
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ '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",
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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',
],
- author='',
- author_email='',
- url='',
- keywords='web pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- 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/__init__.py b/docs/tutorials/wiki/src/models/tutorial/__init__.py
index f2a86df47..f2b3c9568 100644
--- a/docs/tutorials/wiki/src/models/tutorial/__init__.py
+++ b/docs/tutorials/wiki/src/models/tutorial/__init__.py
@@ -11,8 +11,13 @@ def root_factory(request):
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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+ with Configurator(settings=settings) as config:
+ config.include('pyramid_chameleon')
+ config.include('pyramid_tm')
+ config.include('pyramid_retry')
+ config.include('pyramid_zodbconn')
+ config.set_root_factory(root_factory)
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki/src/models/tutorial/models.py b/docs/tutorials/wiki/src/models/tutorial/models.py
index 9761856c6..7c6597afa 100644
--- a/docs/tutorials/wiki/src/models/tutorial/models.py
+++ b/docs/tutorials/wiki/src/models/tutorial/models.py
@@ -10,13 +10,11 @@ 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
frontpage.__name__ = 'FrontPage'
frontpage.__parent__ = app_root
zodb_root['app_root'] = app_root
- import transaction
- transaction.commit()
return zodb_root['app_root']
diff --git a/docs/tutorials/wiki/src/models/tutorial/pshell.py b/docs/tutorials/wiki/src/models/tutorial/pshell.py
new file mode 100644
index 000000000..3d026291b
--- /dev/null
+++ b/docs/tutorials/wiki/src/models/tutorial/pshell.py
@@ -0,0 +1,11 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['models'] = models
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/ie6.css b/docs/tutorials/wiki/src/models/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/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/tutorials/wiki/src/models/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/models/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pylons.css b/docs/tutorials/wiki/src/models/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/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/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/models/tutorial/static/theme.css b/docs/tutorials/wiki/src/models/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki/src/models/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/wiki/src/models/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/models/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki/src/models/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt
index 13b41f823..d63ea8c45 100644
--- a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt
+++ b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt
@@ -1,73 +1,65 @@
-<!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>The Pyramid Web Framework</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="/static/favicon.ico" />
- <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <!--[if lte IE 6]>
- <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" />
- <![endif]-->
-</head>
-<body>
- <div id="wrap">
- <div id="top">
- <div class="top align-center">
- <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
+<!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>Cookiecutter ZODB project for the Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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">ZODB Project</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
+ </div>
+ </div>
</div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org/">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
+ <div class="row">
+ <div class="links">
+ <ul>
+ <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="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
</ul>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</div>
</div>
</div>
- </div>
-</body>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/models/tutorial/tests.py b/docs/tutorials/wiki/src/models/tutorial/tests.py
index 0c5f99575..ca7a47279 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):
@@ -58,4 +14,4 @@ class ViewTests(unittest.TestCase):
from .views import my_view
request = testing.DummyRequest()
info = my_view(request)
- self.assertEqual(info['project'], 'tutorial')
+ self.assertEqual(info['project'], 'myproj')
diff --git a/docs/tutorials/wiki/src/models/tutorial/views.py b/docs/tutorials/wiki/src/models/tutorial/views.py
index 628ce15ed..c1878bdd0 100644
--- a/docs/tutorials/wiki/src/models/tutorial/views.py
+++ b/docs/tutorials/wiki/src/models/tutorial/views.py
@@ -4,4 +4,4 @@ from .models import MyModel
@view_config(context=MyModel, renderer='templates/mytemplate.pt')
def my_view(request):
- return {'project': 'tutorial'}
+ return {'project': 'myproj'}
diff --git a/docs/tutorials/wiki/src/tests/.coveragerc b/docs/tutorials/wiki/src/tests/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki/src/tests/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki/src/tests/.gitignore b/docs/tutorials/wiki/src/tests/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki/src/tests/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki/src/tests/CHANGES.txt b/docs/tutorials/wiki/src/tests/CHANGES.txt
index e14f633ab..14b902fd1 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..8a56d14af 100644
--- a/docs/tutorials/wiki/src/tests/README.txt
+++ b/docs/tutorials/wiki/src/tests/README.txt
@@ -1,4 +1,29 @@
-tutorial README
+myproj
+======
+Getting Started
+---------------
+- Change directory into your newly created project.
+ cd tutorial
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/tests/development.ini b/docs/tutorials/wiki/src/tests/development.ini
index 72bd22e54..228f18f36 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,28 +13,29 @@ 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
+retry.attempts = 3
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -62,4 +63,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..46b1e331b 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -11,25 +11,25 @@ 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
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/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/pytest.ini b/docs/tutorials/wiki/src/tests/pytest.ini
new file mode 100644
index 000000000..8b76bc410
--- /dev/null
+++ b/docs/tutorials/wiki/src/tests/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = *.py
diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py
index 2e7ed2398..7011387f6 100644
--- a/docs/tutorials/wiki/src/tests/setup.py
+++ b/docs/tutorials/wiki/src/tests/setup.py
@@ -9,40 +9,51 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9a',
'pyramid_chameleon',
+ 'pyramid_debugtoolbar',
+ 'pyramid_retry',
+ 'pyramid_tm',
'pyramid_zodbconn',
'transaction',
- 'pyramid_tm',
- 'pyramid_debugtoolbar',
'ZODB3',
'waitress',
'docutils',
- 'WebTest', # add this
- ]
+ 'bcrypt',
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ '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",
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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',
],
- author='',
- author_email='',
- url='',
- keywords='web pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- 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..58635ea74 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/__init__.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/__init__.py
@@ -15,13 +15,18 @@ def root_factory(request):
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
authn_policy = AuthTktAuthenticationPolicy(
'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.add_static_view('static', 'static', cache_max_age=3600)
- config.scan()
- return config.make_wsgi_app()
+ with Configurator(settings=settings) as config:
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(authz_policy)
+ config.include('pyramid_chameleon')
+ config.include('pyramid_tm')
+ config.include('pyramid_retry')
+ config.include('pyramid_zodbconn')
+ config.set_root_factory(root_factory)
+ 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..ebd70e912 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/models.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/models.py
@@ -17,13 +17,11 @@ 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
frontpage.__name__ = 'FrontPage'
frontpage.__parent__ = app_root
zodb_root['app_root'] = app_root
- import transaction
- transaction.commit()
return zodb_root['app_root']
diff --git a/docs/tutorials/wiki/src/tests/tutorial/pshell.py b/docs/tutorials/wiki/src/tests/tutorial/pshell.py
new file mode 100644
index 000000000..3d026291b
--- /dev/null
+++ b/docs/tutorials/wiki/src/tests/tutorial/pshell.py
@@ -0,0 +1,11 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['models'] = models
diff --git a/docs/tutorials/wiki/src/tests/tutorial/security.py b/docs/tutorials/wiki/src/tests/tutorial/security.py
index d88c9c71f..cbb3acd5d 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/security.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/security.py
@@ -1,5 +1,18 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
+import bcrypt
+
+
+def hash_password(pw):
+ hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
+ # return unicode instead of bytes because databases handle it better
+ return hashed_pw.decode('utf-8')
+
+def check_password(expected_hash, pw):
+ if expected_hash is not None:
+ return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8'))
+ return False
+
+USERS = {'editor': hash_password('editor'),
+ 'viewer': hash_password('viewer')}
GROUPS = {'editor':['group:editors']}
def groupfinder(userid, request):
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/tests/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/tests/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/tests/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/ie6.css b/docs/tutorials/wiki/src/tests/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/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/tutorials/wiki/src/tests/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/tests/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/pylons.css b/docs/tutorials/wiki/src/tests/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/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/tutorials/wiki/src/tests/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/theme.css b/docs/tutorials/wiki/src/tests/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki/src/tests/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/wiki/src/tests/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/tests/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt
index c3a0acf6b..eedb83da4 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt
+++ b/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt
@@ -1,58 +1,72 @@
-<!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>${page.__name__} - 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="/static/favicon.ico" />
- <link rel="stylesheet"
- href="/static/pylons.css"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="/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="/static/pyramid-small.png" />
+<!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 rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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>
- </div>
- <div id="middle">
- <div class="middle align-right">
- <div id="left" class="app-welcome align-left">
- Editing <b><span tal:replace="page.__name__">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</div>
</div>
</div>
- <div id="bottom">
- <div class="bottom">
- <form action="${save_url}" method="post">
- <textarea name="body" tal:content="page.data" rows="10"
- cols="60"/><br/>
- <input type="submit" name="form.submitted" value="Save"/>
- </form>
- </div>
- </div>
- </div>
-</body>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt
index 3612dccde..626db6637 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt
+++ b/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt
@@ -1,54 +1,75 @@
-<!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="/static/favicon.ico" />
- <link rel="stylesheet"
- href="/static/pylons.css"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="/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="/static/pyramid-small.png" />
+<!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 rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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>
- </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 class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</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>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt
deleted file mode 100644
index 13b41f823..000000000
--- a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,73 +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>The Pyramid Web Framework</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="/static/favicon.ico" />
- <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <!--[if lte IE 6]>
- <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" />
- <![endif]-->
-</head>
-<body>
- <div id="wrap">
- <div id="top">
- <div class="top align-center">
- <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
- </div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org/">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt
index 90e20764d..f2a9249ef 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt
@@ -1,61 +1,72 @@
-<!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>${page.__name__} - Pyramid tutorial wiki (based on
+<!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>
- <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="/static/favicon.ico" />
- <link rel="stylesheet"
- href="/static/pylons.css"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="/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="/static/pyramid-small.png" />
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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>
+ <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>
+ </div>
+ </div>
</div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-right">
- <div id="left" class="app-welcome align-left">
- Viewing <b><span tal:replace="page.__name__">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div tal:replace="structure content">
- Page text goes here.
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
</div>
</div>
- </div>
-</body>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py
index 5add04c20..098e9c1bd 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/tests.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py
@@ -122,6 +122,17 @@ class EditPageTests(unittest.TestCase):
self.assertEqual(response.location, 'http://example.com/')
self.assertEqual(context.data, 'Hello yo!')
+class SecurityTests(unittest.TestCase):
+ def test_hashing(self):
+ from .security import hash_password, check_password
+ password = 'secretpassword'
+ hashed_password = hash_password(password)
+ self.assertTrue(check_password(hashed_password, password))
+
+ self.assertFalse(check_password(hashed_password, 'attackerpassword'))
+
+ self.assertFalse(check_password(None, password))
+
class FunctionalTests(unittest.TestCase):
viewer_login = '/login?login=viewer&password=viewer' \
@@ -164,6 +175,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..ea2da01af 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/views.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/views.py
@@ -14,7 +14,7 @@ from pyramid.security import (
)
-from .security import USERS
+from .security import USERS, check_password
from .models import Page
# regular expression used to find WikiWords
@@ -37,15 +37,14 @@ 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,12 +57,11 @@ 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
page.__parent__ = context
-
return dict(page=page, save_url=save_url,
logged_in=request.authenticated_userid)
@@ -73,7 +71,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 +84,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 = ''
@@ -94,22 +92,23 @@ def login(request):
if 'form.submitted' in request.params:
login = request.params['login']
password = request.params['password']
- if USERS.get(login) == password:
+ if check_password(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/.coveragerc b/docs/tutorials/wiki/src/views/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki/src/views/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki/src/views/.gitignore b/docs/tutorials/wiki/src/views/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki/src/views/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki/src/views/CHANGES.txt b/docs/tutorials/wiki/src/views/CHANGES.txt
index 1544cf53b..14b902fd1 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..8a56d14af 100644
--- a/docs/tutorials/wiki/src/views/README.txt
+++ b/docs/tutorials/wiki/src/views/README.txt
@@ -1,4 +1,29 @@
-tutorial README
+myproj
+======
+Getting Started
+---------------
+- Change directory into your newly created project.
+ cd tutorial
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki/src/views/development.ini b/docs/tutorials/wiki/src/views/development.ini
index 72bd22e54..228f18f36 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,28 +13,29 @@ 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
+retry.attempts = 3
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -62,4 +63,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..46b1e331b 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -11,25 +11,25 @@ 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
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/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/pytest.ini b/docs/tutorials/wiki/src/views/pytest.ini
new file mode 100644
index 000000000..8b76bc410
--- /dev/null
+++ b/docs/tutorials/wiki/src/views/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = *.py
diff --git a/docs/tutorials/wiki/src/views/setup.py b/docs/tutorials/wiki/src/views/setup.py
index 5ab4f73cd..a11ae6c8f 100644
--- a/docs/tutorials/wiki/src/views/setup.py
+++ b/docs/tutorials/wiki/src/views/setup.py
@@ -9,39 +9,50 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9a',
'pyramid_chameleon',
+ 'pyramid_debugtoolbar',
+ 'pyramid_retry',
+ 'pyramid_tm',
'pyramid_zodbconn',
'transaction',
- 'pyramid_tm',
- 'pyramid_debugtoolbar',
'ZODB3',
'waitress',
'docutils',
- ]
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ '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",
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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',
],
- author='',
- author_email='',
- url='',
- keywords='web pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- 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/__init__.py b/docs/tutorials/wiki/src/views/tutorial/__init__.py
index f2a86df47..f2b3c9568 100644
--- a/docs/tutorials/wiki/src/views/tutorial/__init__.py
+++ b/docs/tutorials/wiki/src/views/tutorial/__init__.py
@@ -11,8 +11,13 @@ def root_factory(request):
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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+ with Configurator(settings=settings) as config:
+ config.include('pyramid_chameleon')
+ config.include('pyramid_tm')
+ config.include('pyramid_retry')
+ config.include('pyramid_zodbconn')
+ config.set_root_factory(root_factory)
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki/src/views/tutorial/models.py b/docs/tutorials/wiki/src/views/tutorial/models.py
index 9761856c6..7c6597afa 100644
--- a/docs/tutorials/wiki/src/views/tutorial/models.py
+++ b/docs/tutorials/wiki/src/views/tutorial/models.py
@@ -10,13 +10,11 @@ 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
frontpage.__name__ = 'FrontPage'
frontpage.__parent__ = app_root
zodb_root['app_root'] = app_root
- import transaction
- transaction.commit()
return zodb_root['app_root']
diff --git a/docs/tutorials/wiki/src/views/tutorial/pshell.py b/docs/tutorials/wiki/src/views/tutorial/pshell.py
new file mode 100644
index 000000000..3d026291b
--- /dev/null
+++ b/docs/tutorials/wiki/src/views/tutorial/pshell.py
@@ -0,0 +1,11 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['models'] = models
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/ie6.css b/docs/tutorials/wiki/src/views/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/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/tutorials/wiki/src/views/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/views/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pylons.css b/docs/tutorials/wiki/src/views/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/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/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki/src/views/tutorial/static/theme.css b/docs/tutorials/wiki/src/views/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki/src/views/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/wiki/src/views/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/views/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt
index 24ed2e592..2db3ca79c 100644
--- a/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt
+++ b/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt
@@ -1,58 +1,69 @@
-<!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>${page.__name__} - Pyramid tutorial wiki (based on
+<!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>
- <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="/static/favicon.ico" />
- <link rel="stylesheet"
- href="/static/pylons.css"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="/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="/static/pyramid-small.png" />
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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>
+ <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>
- </div>
- <div id="middle">
- <div class="middle align-right">
- <div id="left" class="app-welcome align-left">
- Editing <b><span tal:replace="page.__name__">Page Name Goes
- Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</div>
- <div id="right" class="app-welcome align-right"></div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <form action="${save_url}" method="post">
- <textarea name="body" tal:content="page.data" rows="10"
- cols="60"/><br/>
- <input type="submit" name="form.submitted" value="Save"/>
- </form>
</div>
</div>
- </div>
- <div id="footer">
- <div class="footer"
- >&copy; Copyright 2008-2011, Agendaless Consulting.</div>
- </div>
-</body>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt
deleted file mode 100644
index 50102aa20..000000000
--- a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,76 +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>The Pyramid Web Framework</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="/static/favicon.ico" />
- <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <!--[if lte IE 6]>
- <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" />
- <![endif]-->
-</head>
-<body>
- <div id="wrap">
- <div id="top">
- <div class="top align-center">
- <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
- </div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org/">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
- <div id="footer">
- <div class="footer">&copy; Copyright 2008-2012, Agendaless Consulting.</div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
index 424c4302a..1feeab5ef 100644
--- a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt
@@ -1,61 +1,70 @@
-<!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>${page.__name__} - 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="/static/favicon.ico" />
- <link rel="stylesheet"
- href="/static/pylons.css"
- type="text/css" media="screen" charset="utf-8" />
- <!--[if lte IE 6]>
- <link rel="stylesheet"
- href="/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="/static/pyramid-small.png" />
+<!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 rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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">
+ <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>
+ </div>
+ </div>
</div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-right">
- <div id="left" class="app-welcome align-left">
- Viewing <b><span tal:replace="page.__name__">Page Name Goes
- Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right"></div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div tal:replace="structure content">
- Page text goes here.
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
</div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
</div>
</div>
- </div>
- <div id="footer">
- <div class="footer"
- >&copy; Copyright 2008-2011, Agendaless Consulting.</div>
- </div>
-</body>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
</html>
diff --git a/docs/tutorials/wiki/src/views/tutorial/tests.py b/docs/tutorials/wiki/src/views/tutorial/tests.py
index 663c9f405..ca7a47279 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'], 'myproj')
diff --git a/docs/tutorials/wiki/src/views/tutorial/views.py b/docs/tutorials/wiki/src/views/tutorial/views.py
index 61517c31d..fd2b0edc1 100644
--- a/docs/tutorials/wiki/src/views/tutorial/views.py
+++ b/docs/tutorials/wiki/src/views/tutorial/views.py
@@ -24,13 +24,13 @@ 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)
+ return dict(page=context, content=content, edit_url=edit_url)
@view_config(name='add_page', context='.models.Wiki',
renderer='templates/edit.pt')
@@ -42,19 +42,19 @@ 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
page.__parent__ = context
- return dict(page = page, save_url = save_url)
+ return dict(page=page, save_url=save_url)
@view_config(name='edit_page', context='.models.Page',
renderer='templates/edit.pt')
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'))
+ save_url=request.resource_url(context, 'edit_page')) \ No newline at end of file
diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst
index e724f3e18..fdd218add 100644
--- a/docs/tutorials/wiki/tests.rst
+++ b/docs/tutorials/wiki/tests.rst
@@ -1,39 +1,40 @@
+.. _wiki_adding_tests:
+
============
Adding Tests
============
-We will now add tests for the models and the views and a few functional
-tests in the ``tests.py``. Tests ensure that an application works, and
-that it continues to work after some changes are made in the future.
-
+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.
-Test the Models
+Test the models
===============
-We write tests for the model classes and the appmaker. Changing
-``tests.py``, we'll write a separate test class for each model class, and
+We write tests for the ``model`` classes and the ``appmaker``. Changing
+``tests.py``, we'll write a separate test class for each ``model`` class, and
we'll write a test class for the ``appmaker``.
To do so, we'll retain the ``tutorial.tests.ViewTests`` class that was
-generated as part of the ``zodb`` scaffold. We'll add three test
+generated from choosing the ``zodb`` backend option. We'll add three test
classes: one for the ``Page`` model named ``PageModelTests``, one for the
``Wiki`` model named ``WikiModelTests``, and one for the appmaker named
``AppmakerTests``.
-Test the Views
+Test the views
==============
We'll modify our ``tests.py`` file, adding tests for each view function we
-added above. As a result, we'll *delete* the ``ViewTests`` test in the file,
-and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``,
-``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``,
-``view_page``, ``add_page``, and ``edit_page`` views respectively.
-
+added previously. As a result, we'll delete the ``ViewTests`` class that the
+``zodb`` backend option provided, and add four other test classes:
+``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``.
+These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page``
+views.
Functional tests
================
-We test the whole application, covering security aspects that are not
+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.
@@ -41,65 +42,36 @@ can, and so on.
View the results of all our edits to ``tests.py``
=================================================
-Once we're done with the ``tests.py`` module, it will look a lot like the
-below:
+Open the ``tutorial/tests.py`` module, and edit it such that it appears as
+follows:
.. literalinclude:: src/tests/tutorial/tests.py
- :linenos:
- :language: python
+ :linenos:
+ :language: python
-Running the Tests
+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``.
+We can run these tests by using ``pytest`` similarly to how we did in
+:ref:`running_tests`. Courtesy of the cookiecutter, our testing dependencies have
+already been satisfied and ``pytest`` and coverage have already been
+configured, so we can jump right to running tests.
-.. literalinclude:: src/tests/setup.py
- :linenos:
- :language: python
- :lines: 11-22
- :emphasize-lines: 11
+On Unix:
-After we've added a dependency on WebTest in ``setup.py``, we need to rerun
-``setup.py develop`` to get WebTest installed into our virtualenv. Assuming
-our shell's current working directory is the "tutorial" distribution
-directory:
+.. code-block:: bash
-On UNIX:
-
-.. code-block:: text
-
- $ $VENV/bin/python setup.py develop
+ $VENV/bin/pytest -q
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:
-
-On UNIX:
-
-.. code-block:: text
-
- $ $VENV/bin/python setup.py test -q
-
-On Windows:
-
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
+ %VENV%\Scripts\pytest -q
-The expected result looks something like:
+The expected result should look like the following:
.. code-block:: text
- .........
- ----------------------------------------------------------------------
- Ran 23 tests in 1.653s
-
- OK
+ .........................
+ 25 passed in 6.87 seconds
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
new file mode 100644
index 000000000..3f2fcec83
--- /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: 19-21
+ :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: 17-19
+ :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 import:
+
+.. literalinclude:: src/authentication/tutorial/views/default.py
+ :lines: 5-9
+ :lineno-match:
+ :emphasize-lines: 2
+ :language: python
+
+Change the highlighted line.
+
+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 2e35574fd..234f40e3b 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -1,413 +1,263 @@
.. _wiki2_adding_authorization:
====================
-Adding Authorization
+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.
+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 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 access control with the following steps:
-We will implement the 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 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 user principals
+-------------------
-* 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``).
+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.
-Access Control
---------------
-
-Add users and groups
-~~~~~~~~~~~~~~~~~~~~
-
-Create a new ``tutorial/tutorial/security.py`` module with the
-following content:
+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:
-
-- 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``.
+Only the highlighted lines need to be added.
-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.
+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.
-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 an ACL
-~~~~~~~~~~
+Add the authorization policy
+----------------------------
-Open ``tutorial/tutorial/models.py`` and add the following import
-statement at the head:
+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.
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 1-4
- :linenos:
- :language: python
-
-Add the following class definition:
-
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 33-37
- :linenos:
- :language: python
+In the file ``tutorial/security.py``, notice the following lines:
-We import :data:`~pyramid.security.Allow`, an action that
-means that permission is allowed:, and
-:data:`~pyramid.security.Everyone`, a special :term:`principal`
-that is associated to all requests. Both are used in the
-:term:`ACE` entries that make up the ACL.
-
-The ACL is a list that needs to be named `__acl__` and be an
-attribute of a class. We define an :term:`ACL` with two
-:term:`ACE` entries: the first entry allows any user the `view`
-permission. The second entry allows the ``group:editors``
-principal the `edit` permission.
-
-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.
-
-Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory``
-parameter to our :term:`Configurator` constructor, that points to
-the class we created above:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 24-25
- :linenos:
+.. literalinclude:: src/authorization/tutorial/security.py
+ :lines: 38-40
+ :lineno-match:
:emphasize-lines: 2
:language: python
-(Only the highlighted line needs to be added.)
-
-We are now providing the ACL to the application. See
-:ref:`assigning_acls` for more information about what an
-:term:`ACL` represents.
-
-.. 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.
-
-Add Authentication and Authorization Policies
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Open ``tutorial/__init__.py`` and
-add these import statements:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 2-3,7
- :linenos:
- :language: python
-
-Now add those policies to the configuration:
+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/__init__.py
- :lines: 21-27
- :linenos:
- :emphasize-lines: 1-3,6-7
- :language: python
-
-(Only the highlighted lines need to be added.)
-We are enabling an ``AuthTktAuthenticationPolicy``, which is based in an
-auth ticket that may be included in the request.
-We are also enabling an ``ACLAuthorizationPolicy``, which uses an ACL to
-determine the *allow* or *deny* outcome for a view.
+Add resources and ACLs
+----------------------
-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.
+Resources are the hidden gem of :app:`Pyramid`. You've made it!
-Add permission declarations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+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.
-Add a ``permission='edit'`` parameter to the ``@view_config``
-decorator for ``add_page()`` and ``edit_page()``, for example:
-
-.. code-block:: python
- :linenos:
- :emphasize-lines: 2
-
- @view_config(route_name='add_page', renderer='templates/edit.pt',
- permission='edit')
-
-(Only the highlighted line needs to be added.)
-
-The result is that only users who possess the ``edit``
-permission at the time of the request may invoke those two views.
-
-Add a ``permission='view'`` parameter to the ``@view_config``
-decorator for ``view_wiki()`` and ``view_page()``, like this:
-
-.. code-block:: python
- :linenos:
- :emphasize-lines: 2
+Our wiki has two resources:
- @view_config(route_name='view_page', renderer='templates/view.pt',
- permission='view')
+#. 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.
-This allows anyone to invoke these two views.
+.. note::
-We are done with the changes needed to control access. The
-changes that follow will add the login and logout feature.
+ 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.
-Login, Logout
--------------
+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!
-Add routes for /login and /logout
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Go back to ``tutorial/tutorial/__init__.py`` and add these two
-routes:
+Open the file ``tutorial/routes.py`` and edit the following lines:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 31-32
+.. literalinclude:: src/authorization/tutorial/routes.py
:linenos:
+ :emphasize-lines: 1-11,17-
:language: python
-.. note:: The preceding lines must be added *before* the following
- ``view_page`` route definition:
-
- .. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 32
- :linenos:
- :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
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We'll add a ``login`` view which renders a login form and processes
-the post from the login form, checking credentials.
+The highlighted lines need to be edited or added.
-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.
+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:
-Add the following import statements to the
-head of ``tutorial/tutorial/views.py``:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 9-19
- :linenos:
- :emphasize-lines: 3,6-9,11
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 30-38
+ :lineno-match:
+ :emphasize-lines: 5-9
:language: python
-(Only the highlighted lines need to be added.)
-
-: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:
+The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by
+declaring a ``factory`` on the route:
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 91-123
- :linenos:
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 18-19
+ :lineno-match:
+ :emphasize-lines: 1-2
:language: python
-``login()`` is decorated with 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
- an :term:`forbidden view`. ``login()`` will be invoked
- when a users tries to execute a view callable that
- they are not allowed to. For example, if a user has not logged in
- and tries to add or edit a Wiki page, he will be shown the
- login form before being allowed to continue on.
-
-The order of these two :term:`view configuration` decorators
-is unimportant.
-
-``logout()`` is decorated with a ``@view_config`` decorator
-which associates it with the ``logout`` route. It will be
-invoked when we visit ``/logout``.
-
-Add the ``login.pt`` Template
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Create ``tutorial/tutorial/templates/login.pt`` with the following
-content:
-
-.. literalinclude:: src/authorization/tutorial/templates/login.pt
- :language: xml
-
-The above template is referred to within the login view we just
-added to ``views.py``.
+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.
-Return a logged_in flag to the renderer
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Add a ``logged_in`` parameter to the return value of
-``view_page()``, ``edit_page()`` and ``add_page()``,
-like this:
-
-.. code-block:: python
- :linenos:
- :emphasize-lines: 4
-
- return dict(page = page,
- content = content,
- edit_url = edit_url,
- logged_in = request.authenticated_userid)
-
-(Only the highlighted line needs to be added.)
-
-The :meth:`~pyramid.request.Request.authenticated_userid` property will be
-``None`` if the user is not authenticated.
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 47-
+ :lineno-match:
+ :emphasize-lines: 5-10
+ :language: python
-Add a "Logout" link when logged in
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and
+``edit_page`` routes by declaring a ``factory`` on the routes:
-Open ``tutorial/tutorial/templates/edit.pt`` and
-``tutorial/tutorial/templates/view.pt`` and add this within the
-``<div id="right" class="app-welcome align-right">`` div:
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 17-21
+ :lineno-match:
+ :emphasize-lines: 1,4-5
+ :language: python
-.. code-block:: xml
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
+Add view permissions
+--------------------
-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.
+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.
-Seeing Our Changes
-------------------
+Open the file ``tutorial/views/default.py``.
-Our ``tutorial/tutorial/__init__.py`` will look something like this
-when we're done:
+First, you can drop a few imports that are no longer necessary:
-.. 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: 5-7
+ :lineno-match:
+ :emphasize-lines: 1
:language: python
-(Only the highlighted lines need to be added.)
-
-Our ``tutorial/tutorial/models.py`` will look something like this
-when we're done:
+Edit the ``view_page`` view to declare the ``view`` permission, and remove the
+explicit checks within the view:
-.. literalinclude:: src/authorization/tutorial/models.py
- :linenos:
- :emphasize-lines: 1-4,33-37
+.. 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.)
+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.
-Our ``tutorial/tutorial/views.py`` will look something like this
-when we're done:
+Edit the ``edit_page`` view to declare the ``edit`` permission:
-.. literalinclude:: src/authorization/tutorial/views.py
- :linenos:
- :emphasize-lines: 11,14-19,25,31,37,58,61,73,76,88,91-117,119-123
+.. 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.)
+Edit the ``add_page`` view to declare the ``create`` permission:
-Our ``tutorial/tutorial/templates/edit.pt`` template will look
-something like this when we're done:
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 52-56
+ :lineno-match:
+ :emphasize-lines: 1-2,4
+ :language: python
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :linenos:
- :emphasize-lines: 41-43
- :language: xml
+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.
-(Only the highlighted lines need to be added.)
+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.
-Our ``tutorial/tutorial/templates/view.pt`` template will look
-something like this when we're done:
+The final ``tutorial/views/default.py`` should look like the following:
-.. literalinclude:: src/authorization/tutorial/templates/view.pt
+.. literalinclude:: src/authorization/tutorial/views/default.py
:linenos:
- :emphasize-lines: 41-43
- :language: xml
-
-(Only the highlighted lines need to be added.)
+ :language: python
-Viewing the Application in a Browser
+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, check 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.
-
-- 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.
-
-- 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.
+: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/background.rst b/docs/tutorials/wiki2/background.rst
index 1f9582903..c14d3cb7d 100644
--- a/docs/tutorials/wiki2/background.rst
+++ b/docs/tutorials/wiki2/background.rst
@@ -1,15 +1,19 @@
+.. _wiki2_background:
+
==========
Background
==========
-This 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
-URLs to code.
+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
+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.
+To code along with this tutorial, the developer will need a Unix
+machine with development tools (macOS with XCode, any Linux or BSD
+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 05781c044..f3a9db223 100644
--- a/docs/tutorials/wiki2/basiclayout.rst
+++ b/docs/tutorials/wiki2/basiclayout.rst
@@ -1,250 +1,331 @@
+.. _wiki2_basic_layout:
+
============
Basic Layout
============
-The starter files generated by the ``alchemy`` scaffold are very basic, but
-they provide a good orientation for the high-level patterns common to most
-:term:`url dispatch` -based :app:`Pyramid` projects.
+The starter files generated from choosing the ``sqlalchemy`` backend option in
+the cookiecutter are very basic, but they provide a good orientation for the
+high-level patterns common to most :term:`URL dispatch`-based :app:`Pyramid`
+projects.
-Application Configuration with ``__init__.py``
+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
-it's contained within is a package, and to contain configuration code.
+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
+ :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
+ :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 using a context manager:
+
+.. 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 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
-``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.
+.. 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
-Next, include :term:`Chameleon` templating bindings so that we can use
-renderers with the ``.pt`` extension within our project.
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 17
- :language: py
+Route declarations
+------------------
-``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with
-two arguments: ``static`` (the name), and ``static`` (the path):
+Open the ``tutorial/routes.py`` file. It should already contain the following:
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 18
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/routes.py
+ :linenos:
+ :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 ``/``:
-
- .. literalinclude:: src/basiclayout/tutorial/__init__.py
- :lines: 19
- :language: py
-
-Since this route has a ``pattern`` equalling ``/`` 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:
+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: 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, i.e. ``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`.
+: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.
+The sample ``my_view()`` created by the cookiecutter 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.
+Content models with the ``models`` package
+------------------------------------------
-Open ``tutorial/tutorial/models.py``. It should already contain the following:
+In an SQLAlchemy-based application, a *model* object is an object composed by
+querying the SQL database. The ``models`` package is where the ``alchemy``
+cookiecutter put the classes that implement our models.
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :linenos:
- :language: py
+First, open ``tutorial/models/meta.py``, which should already contain the
+following:
-Let's examine this in detail. First, we need some imports to support later code:
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :linenos:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :end-before: DBSession
- :linenos:
- :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.
-Next we set up a SQLAlchemy ``DBSession`` object:
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :end-before: metadata
+ :linenos:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :lines: 16
- :language: py
+Next we create a ``metadata`` object from the class
+:class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value
+for the ``naming_convention`` argument.
-``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.
+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.
-We also need to create a declarative ``Base`` object to use as a
-base class for our model:
+.. literalinclude:: src/basiclayout/tutorial/models/meta.py
+ :lines: 15-16
+ :lineno-match:
+ :language: py
- .. literalinclude:: src/basiclayout/tutorial/models.py
- :lines: 17
- :language: py
+Next open ``tutorial/models/mymodel.py``, which should already contain the
+following:
-Our model classes will inherit from this ``Base`` class so they can be
-associated with our particular database connection.
+.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
+ :linenos:
+ :language: py
-To give a simple example of a model class, we define one named ``MyModel``:
+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.py
- :pyobject: MyModel
- :linenos:
- :language: py
+.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
+ :pyobject: MyModel
+ :lineno-match:
+ :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:
- .. code-block:: python
+ .. code-block:: python
- johnny = MyModel(name="John Doe", value=10)
+ johnny = MyModel(name="John Doe", value=10)
The ``MyModel`` class has a ``__tablename__`` attribute. This informs
SQLAlchemy which table to use to store the data representing instances of this
class.
+Finally, open ``tutorial/models/__init__.py``, which should already
+contain the following:
+
+.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
+ :linenos:
+ :language: py
+
+Our ``models/__init__.py`` module defines the primary API we will use for
+configuring the database connections within our application, and it contains
+several functions we will cover below.
+
+As we mentioned above, the purpose of the ``models.meta.metadata`` object is to
+describe the schema of the database. This is done by defining models that
+inherit from the ``Base`` 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:
+ :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:
+ :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:
+ :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:
+ :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 e30af12b2..9159027c4 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -1,126 +1,356 @@
+.. _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.
+The first change we'll make to our stock cookiecutter-generated application will
+be to define a wiki page :term:`domain model`.
+
+.. note::
+
+ 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.
+
+
+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 cookiecutter; it doesn't know about our
+custom application requirements.
-Making Edits to ``models.py``
------------------------------
+We need to add a dependency, the `bcrypt <https://pypi.org/project/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: 13
+ :language: python
+
+Only the highlighted line needs to be added.
.. 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.
+ 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.
+
+
+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
+
+ $VENV/bin/pip install -e .
+
+On Windows:
+
+.. code-block:: doscon
+
+ %VENV%\Scripts\pip install -e .
+
+Success executing this command will end with a line to the console something
+like the following.
+
+.. code-block:: text
+
+ Successfully installed bcrypt-3.1.4 cffi-1.11.5 pycparser-2.18 tutorial
+
+
+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. 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.
+
+
+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
+
+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.
-Open ``tutorial/tutorial/models.py`` file and edit it to look like the
-following:
-.. literalinclude:: src/models/tutorial/models.py
- :linenos:
- :language: py
- :emphasize-lines: 20-22,25
+Edit ``models/__init__.py``
+===========================
-(The highlighted lines are the ones that need to be changed.)
+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.
-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.
+Open the ``tutorial/models/__init__.py`` file and edit it to look like
+the following:
-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`.
+.. literalinclude:: src/models/tutorial/models/__init__.py
+ :linenos:
+ :language: py
+ :emphasize-lines: 8,9
-.. literalinclude:: src/models/tutorial/models.py
- :pyobject: Page
- :linenos:
- :language: python
+Here we align our imports with the names of the models, ``Page`` and ``User``.
-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.
-Changing ``scripts/initializedb.py``
-------------------------------------
+.. _wiki2_migrate_database_alembic:
+
+Migrate the database with Alembic
+=================================
+
+Now that we have written our models, we need to modify the database schema to reflect the changes to our code. Let's generate a new revision, then upgrade the database to the latest revision (head).
+
+On Unix:
+
+.. code-block:: bash
+
+ $VENV/bin/alembic -c development.ini revision --autogenerate \
+ -m "use new models Page and User"
+ $VENV/bin/alembic -c development.ini upgrade head
+
+On Windows:
+
+.. code-block:: doscon
+
+ %VENV%\Scripts\alembic -c development.ini revision \
+ --autogenerate -m "use new models Page and User"
+ %VENV%\Scripts\alembic -c development.ini upgrade head
+
+Success executing these commands will generate output similar to the following.
+
+.. code-block:: text
+
+ 2018-06-29 01:28:42,407 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:28:42,407 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:28:42,408 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:28:42,408 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:28:42,409 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-29 01:28:42,409 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,410 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT alembic_version.version_num
+ FROM alembic_version
+ 2018-06-29 01:28:42,410 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,411 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT name FROM sqlite_master WHERE type='table' ORDER BY name
+ 2018-06-29 01:28:42,412 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,413 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("models")
+ 2018-06-29 01:28:42,413 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA foreign_key_list("models")
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,415 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_list("models")
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_info("my_index")
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_list("models")
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_info("my_index")
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ Generating /<somepath>/tutorial/tutorial/alembic/versions/20180629_23e9f8eb6c28.py ... done
+
+.. code-block:: text
+
+ 2018-06-29 01:29:37,957 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT alembic_version.version_num
+ FROM alembic_version
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,963 INFO [sqlalchemy.engine.base.Engine:1151][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)
+ )
+
+
+ 2018-06-29 01:29:37,963 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,966 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,968 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ CREATE TABLE pages (
+ id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ data TEXT NOT NULL,
+ creator_id INTEGER NOT NULL,
+ CONSTRAINT pk_pages PRIMARY KEY (id),
+ CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id),
+ CONSTRAINT uq_pages_name UNIQUE (name)
+ )
+
+
+ 2018-06-29 01:29:37,968 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ DROP INDEX my_index
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ DROP TABLE models
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,971 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] UPDATE alembic_version SET version_num='23e9f8eb6c28' WHERE alembic_version.version_num = 'b6b22ae3e628'
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+
+
+.. _wiki2_alembic_overview:
+
+Alembic overview
+----------------
+
+Let's briefly discuss our configuration for Alembic.
+
+In the alchemy cookiecutter's ``development.ini`` file, the setting for ``script_location`` configures Alembic to look for the migration script in the directory ``tutorial/alembic``.
+By default Alembic stores the migration files one level deeper in ``tutorial/alembic/versions``.
+These files are generated by Alembic, then executed when we run upgrade or downgrade migrations.
+The setting ``file_template`` provides the format for each migration's file name.
+We've configured the ``file_template`` setting to make it somewhat easy to find migrations by file name.
+
+At this point in this tutorial, we have two migration files.
+Examine them to see what Alembic will do when you upgrade or downgrade the database to a specific revision.
+Notice the revision identifiers and how they relate to one another in a chained sequence.
+
+.. seealso:: For further information, see the `Alembic documentation <http://alembic.zzzcomputing.com/en/latest/>`_.
+
+
+Edit ``scripts/initialize_db.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).
+directory of your ``tutorial`` package is a file named ``initialize_db.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.
+
+.. note::
+
+ 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.
-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``.
+Since we've changed our model, we need to make changes to our
+``initialize_db.py`` script. In particular, we'll replace our import of
+``MyModel`` with those of ``User`` and ``Page``. We'll also change the 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
+Open ``tutorial/scripts/initialize_db.py`` and edit it to look like the
following:
-.. literalinclude:: src/models/tutorial/scripts/initializedb.py
- :linenos:
- :language: python
- :emphasize-lines: 14,36
-
-(Only the highlighted lines need to be changed.)
-
-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 :ref:`initialize_db_wiki2` for instructions.
-
-Success will look something like this::
-
- 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread]
- PRAGMA table_info("pages")
- 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread]
- CREATE TABLE pages (
- id INTEGER NOT NULL,
- name TEXT,
- data TEXT,
- PRIMARY KEY (id),
- UNIQUE (name)
- )
-
-
- 2011-11-27 01:22:45,278 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2011-11-27 01:22:45,397 INFO [sqlalchemy.engine.base.Engine][MainThread]
- COMMIT
- 2011-11-27 01:22:45,400 INFO [sqlalchemy.engine.base.Engine][MainThread]
- BEGIN (implicit)
- 2011-11-27 01:22:45,401 INFO [sqlalchemy.engine.base.Engine][MainThread]
- INSERT INTO pages (name, data) VALUES (?, ?)
- 2011-11-27 01:22:45,401 INFO [sqlalchemy.engine.base.Engine][MainThread]
- ('FrontPage', 'This is the front page')
- 2011-11-27 01:22:45,402 INFO [sqlalchemy.engine.base.Engine][MainThread]
- COMMIT
-
-Viewing the Application in a Browser
-------------------------------------
+.. literalinclude:: src/models/tutorial/scripts/initialize_db.py
+ :linenos:
+ :language: python
+ :emphasize-lines: 11-24
+
+Only the highlighted lines need to be changed.
+
+
+Populating the database
+=======================
+
+Because our model has changed, and to repopulate the database, we
+need to rerun the ``initialize_tutorial_db`` command to pick up the changes
+we've made to the initialize_db.py file. See :ref:`initialize_db_wiki2` for instructions.
+
+Success will look something like this:
+
+.. code-block:: text
+
+ 2018-06-29 01:30:39,326 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:30:39,326 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:30:39,327 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:30:39,327 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:30:39,328 INFO [sqlalchemy.engine.base.Engine:682][MainThread] BEGIN (implicit)
+ 2018-06-29 01:30:39,329 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,329 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('editor', 'editor', '$2b$12$PlaJSN7goVbyx8OFs8yAju9n5gHGdI6PZ2QRJGM2jDCiEU4ItUNxy')
+ 2018-06-29 01:30:39,330 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,330 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('basic', 'basic', '$2b$12$MvXdM8jlkbjEyPZ6uXzRg.yatZZK8jCwfPaM7kFkmVJiJjRoCCvmW')
+ 2018-06-29 01:30:39,331 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,331 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('FrontPage', 'This is the front page', 1)
+ 2018-06-29 01:30:39,332 INFO [sqlalchemy.engine.base.Engine:722][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`) and visit http://localhost:6543, you'll wind up with a Python traceback on
+your console that ends with this exception:
.. code-block:: text
- ImportError: cannot import name MyModel
+ AttributeError: module 'tutorial.models' has no attribute 'MyModel'
This will also happen if you attempt to run the tests.
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index 49dbed50f..d10d862f5 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -1,366 +1,477 @@
+.. _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}``,
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'}``
+against ``/foo/bar`` and the ``matchdict`` would look like ``{'one':'foo',
+'two':'bar'}``.
+
-Declaring Dependencies in Our ``setup.py`` File
-===============================================
+Adding the ``docutils`` dependency
+==================================
-The view code in our application will depend on a package which is not a
-dependency of the original "tutorial" application. The original "tutorial"
-application was generated by the ``pcreate`` command; it doesn't know
-about our custom application requirements.
+Remember in the previous chapter we added a new dependency 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`` parameter in ``setup()``.
+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/views/setup.py
:linenos:
+ :emphasize-lines: 13
:language: python
- :emphasize-lines: 20
-(Only the highlighted line needs to be added.)
+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 rerun ``python
-setup.py develop`` 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
+=============
-On UNIX:
+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.
-.. code-block:: text
+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.
- $ cd tutorial
- $ $VENV/bin/python setup.py develop
-On Windows:
+Adding routes to ``routes.py``
+==============================
-.. code-block:: text
+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.
- c:\pyramidtut> cd tutorial
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route`
+calls which serve to add routes to our application. First we'll get rid of the
+existing route created by the template using the name ``'home'``. It's only an
+example and isn't relevant to our application.
-Success executing this command will end with a line to the console something
-like::
+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.
- Finished processing dependencies for tutorial==0.0
+#. 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'``.
-Changing the ``views.py`` File
-==============================
+#. 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'``.
+
+#. 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'``.
+
+#. 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,12,15-70
-(The highlighted lines are the ones that need to be added or edited.)
+The highlighted lines are the ones that need to be added or edited.
-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.
+.. 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,14-
+
+The highlighted lines need to be added or edited.
+
+We added some imports, and created a regular expression to find "WikiWords".
-Then we added four :term:`view callable` functions to our ``views.py``
-module:
+We got rid of the ``my_view`` view function and its decorator that was added
+when originally rendered after we selected the ``sqlalchemy`` backend option in
+the cookiecutter. It was only an example and isn't relevant to our
+application. We also deleted the ``db_err_msg`` string.
+
+Then we added four :term:`view callable` functions to our ``views/default.py``
+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 and show the resulting ``views.py`` file
-afterward.
+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`` except 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
-------------------------------
-``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
-a URL which represents the path to our "FrontPage".
+Following is the code for the ``view_wiki`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 18-21
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 17-20
+ :lineno-match:
:linenos:
:language: python
-``view_wiki()`` returns an instance of the
+``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 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).
+the :class:`pyramid.interfaces.IResponse` interface, like
+: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.
-It uses the :meth:`pyramid.request.Request.route_url` API to construct a
-URL to the ``FrontPage`` page (e.g. ``http://localhost:6543/FrontPage``), which
-is used as the "location" of the ``HTTPFound`` response, forming an HTTP redirect.
The ``view_page`` view function
-------------------------------
-``view_page()`` is used to display a single page of our
-wiki. It renders the :term:`reStructuredText` body of a page (stored as
-the ``data`` attribute of a ``Page`` model object) as HTML. Then it substitutes an
-HTML anchor for each *WikiWord* reference in the rendered HTML using a
-compiled regular expression.
+Here is the code for the ``view_page`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 23-43
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 22-42
+ :lineno-match:
:linenos:
:language: python
-The ``check()`` function is used as the first argument to
+``view_page()`` is used to display a single page of our wiki. It renders the
+:term:`reStructuredText` body of a page (stored as the ``data`` attribute of a
+``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 ``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()``.
-
-The ``add_page`` view function
-------------------------------
-
-``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.
+renderer used will be the ``view.jinja2`` template, as indicated in
+the ``@view_config`` decorator that is applied to ``view_page()``.
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 45-56
- :linenos:
- :language: python
+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``.
-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 scrape 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
-------------------------------
-``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.
+Here is the code for the ``edit_page`` view function and its decorator:
-.. literalinclude:: src/views/tutorial/views.py
- :lines: 58-70
+.. literalinclude:: src/views/tutorial/views/default.py
+ :lines: 44-56
+ :lineno-match:
:linenos:
:language: python
-If the view execution *is* a result of a form submission (i.e. the expression
+``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
``body`` element of the request parameters and sets it as the ``data``
attribute of the page object. It then redirects to the ``view_page`` view
of the wiki page.
-If the view execution is *not* a result of a form submission (i.e. the
+If the view execution is *not* a result of a form submission (i.e., the
expression ``'form.submitted' in request.params`` is ``False``), the view
simply renders the edit form, passing the page object and a ``save_url``
which will be used as the action of the generated form.
-Adding Templates
+.. 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 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:
- :language: xml
+ :emphasize-lines: 11,35-37
+ :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 (rows 45-47). ``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 (rows 49-51).
+- 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 <http://jinja.pocoo.org/>`_ 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: xml
-
-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:
-
-- A 10 row by 60 column ``textarea`` field named ``body`` that is filled
- with any existing page data when it is rendered (rows 46-47).
-- A submit button that has the name ``form.submitted`` (row 48).
-
-The form POSTs back to the "save_url" argument supplied
-by the view (row 45). The view will use the ``body`` and
-``form.submitted`` values.
-
-.. note:: Our templates use a ``request`` object that
- none of our tutorial views return in their dictionary.
- ``request`` is one of several
- names that are available "by default" in a template when a template
- renderer is used. See :ref:`renderer_system_values` for
- information about other names that are available by default
- when a template is used as a renderer.
-
-Static Assets
--------------
-
-Our templates name a single static asset named ``pylons.css``. We don't need
-to create this file within our package's ``static`` directory because it was
-provided at the time we created the project. This file is a little too long
-to replicate within the body of this guide, however it is available `online
-<https://github.com/Pylons/pyramid/blob/master/docs/tutorials/wiki2/src/views/tutorial/static/pylons.css>`_.
-
-This CSS file will be accessed via
-e.g. ``http://localhost:6543/static/pylons.css`` by virtue of the call to
-``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'``.
+ :language: html
-#. 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'``.
+This template is used by ``view_page()`` for displaying a single wiki page.
-#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the
- route named ``add_page``. This is the add view for a new page. It maps
- to our ``add_page`` view callable by virtue of the ``@view_config``
- attached to the ``add_page`` view function indicating
- ``route_name='add_page'``.
+- 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 (lines 8-10).
+
+
+The ``edit.jinja2`` template
+----------------------------
+
+Create ``tutorial/templates/edit.jinja2`` and add the following content:
+
+.. literalinclude:: src/views/tutorial/templates/edit.jinja2
+ :linenos:
+ :emphasize-lines: 1,3,12,14,17
+ :language: html
+
+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:
+
+- 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 ``/{pagename}/edit_page`` to the
- route named ``edit_page``. This is the edit view for a page. It maps
- to our ``edit_page`` view callable by virtue of the ``@view_config``
- attached to the ``edit_page`` view function indicating
- ``route_name='edit_page'``.
-As a result of our edits, the ``__init__.py`` file should look
-something like:
+The ``404.jinja2`` template
+---------------------------
-.. literalinclude:: src/views/tutorial/__init__.py
+Replace ``tutorial/templates/404.jinja2`` with the following content:
+
+.. literalinclude:: src/views/tutorial/templates/404.jinja2
:linenos:
+ :language: html
+
+This template is linked from the ``notfound_view`` defined in
+``tutorial/views/notfound.py`` as shown here:
+
+.. literalinclude:: src/views/tutorial/views/notfound.py
+ :linenos:
+ :emphasize-lines: 6
:language: python
- :emphasize-lines: 19-22
-(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 selecting the backend option of ``sqlalchemy``, 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
+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, check that the result is as expected:
+each of the following URLs, checking that the result is as expected:
-- http://localhost:6543 in a browser invokes the
- ``view_wiki`` view. This always redirects to the ``view_page`` view
- of the FrontPage page object.
+- 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 in a browser 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 in a browser
- invokes the edit view for the front page object.
+- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for
+ the ``FrontPage`` page object.
-- http://localhost:6543/add_page/SomePageName in a
- browser invokes the add view for a page.
+- 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.
-- 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`.
+- 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`.
diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst
index df2c83398..e3b35d24a 100644
--- a/docs/tutorials/wiki2/design.rst
+++ b/docs/tutorials/wiki2/design.rst
@@ -1,148 +1,157 @@
-==========
+.. _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
+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 an SQLite database to hold our wiki data, and we'll be using
+:term:`SQLAlchemy` to access the data in this database. We will also use :term:`Alembic` for database migrations, including initialization of the SQLite database.
-We'll be using a 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 will define two tables:
-Within the database, we define a single table named `pages`, whose elements
-will store the wiki pages. There are two columns: `name` and `data`.
+- 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.
-A page named ``FrontPage`` containing the text *This is the front page*, will
+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 for 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 users names to their corresponding passwords.
-
-- GROUPS, a dictionary mapping user names to a list of groups they belong to.
-
-- ``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 is | | | |
-| | 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 is | | | |
-| | submitted, | | | |
-| | redirect to | | | |
-| | /PageName | | | |
-+----------------------+-----------------------+-------------+------------+------------+
-| /login | Display login form, | login | login.pt | |
-| | Forbidden [3]_ | | | |
-| | | | | |
-| | If the form is | | | |
-| | submitted, | | | |
-| | authenticate. | | | |
-| | | | | |
-| | - If authentication | | | |
-| | successful, | | | |
-| | 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 3b048a141..0eff63461 100644
--- a/docs/tutorials/wiki2/distributing.rst
+++ b/docs/tutorials/wiki2/distributing.rst
@@ -1,42 +1,40 @@
+.. _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.
+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 contains the ``tutorial`` package and the
+``setup.py`` file.
-On UNIX:
+On Unix:
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/python setup.py sdist
+ $VENV/bin/python setup.py sdist
On Windows:
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut> %VENV%\Scripts\python setup.py sdist
+ %VENV%\Scripts\python setup.py sdist
The output of such a command will be something like:
.. code-block:: text
- running sdist
- # ... more output ...
- creating dist
- tar -cf dist/tutorial-0.0.tar tutorial-0.0
- gzip -f9 dist/tutorial-0.0.tar
- 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 <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 download from
-PyPI.
-
+ running sdist
+ # more output
+ creating dist
+ 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
+``pip install`` command directly at it. Or you can upload it to `PyPI
+<https://pypi.org/>`_ and share it with the rest of the world, where
+it can be downloaded via ``pip install`` remotely like any other package people
+download from PyPI.
diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst
index 0a614cb23..40a194155 100644
--- a/docs/tutorials/wiki2/index.rst
+++ b/docs/tutorials/wiki2/index.rst
@@ -1,16 +1,14 @@
.. _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
-: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.
+This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based
+:app:`Pyramid` application to a developer familiar with Python. When 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
-<https://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src>`_,
+tutorial can be browsed on GitHub at `GitHub <https://github.com/Pylons/pyramid/>`_ for a specific branch or version under ``docs/tutorials/wiki2/src``,
which corresponds to the same location if you have Pyramid sources.
.. toctree::
@@ -22,8 +20,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 e21bf7108..924927cd4 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -1,389 +1,558 @@
+.. _wiki2_installation:
+
============
Installation
============
-Before You Begin
-================
+Before you begin
+----------------
This tutorial assumes that you have already followed the steps in
-:ref:`installing_chapter`, thereby satisfying the following
-requirements.
+:ref:`installing_chapter`, except **do not create a virtual environment or
+install Pyramid**. Thereby you will satisfy the following requirements.
-* Python interpreter is installed on your operating system
-* :term:`setuptools` or :term:`distribute` is installed
-* :term:`virtualenv` is installed
+* A Python interpreter is installed on your operating system.
+* You've satisfied the :ref:`requirements-for-installing-packages`.
-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 absolute path of the
-virtual environment.
+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 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:
+
+.. code-block:: bash
+
+ sudo apt-get install libsqlite3-dev
+
-On UNIX
+Install cookiecutter
+--------------------
+We will use a :term:`cookiecutter` to create a Python package project from a Python package project template. See `Cookiecutter Installation <https://cookiecutter.readthedocs.io/en/latest/installation.html>`_ for instructions.
+
+
+Generate a Pyramid project from a cookiecutter
+----------------------------------------------
+
+We will create a Pyramid project in your home directory for Unix or at the root for Windows. It is assumed you know the path to where you installed ``cookiecutter``. Issue the following commands and override the defaults in the prompts as follows.
+
+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.
+ cd ~
+ cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout master
On Windows
^^^^^^^^^^
-Set the `VENV` environment variable.
+.. code-block:: doscon
+
+ cd \
+ cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout master
+
+On all operating systems
+^^^^^^^^^^^^^^^^^^^^^^^^
+If prompted for the first item, accept the default ``yes`` by hitting return.
.. code-block:: text
- c:\> set VENV=c:\pyramidtut
+ You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
+ Is it okay to delete and re-clone it? [yes]: yes
+ project_name [Pyramid Scaffold]: myproj
+ repo_name [myproj]: tutorial
+ Select template_language:
+ 1 - jinja2
+ 2 - chameleon
+ 3 - mako
+ Choose from 1, 2, 3 [1]: 1
+ Select backend:
+ 1 - none
+ 2 - sqlalchemy
+ 3 - zodb
+ Choose from 1, 2, 3 [1]: 2
+
+
+Change directory into your newly created project
+------------------------------------------------
+
+On Unix
+^^^^^^^
-Versions of Python use different paths, so you will need to adjust the
-path to the command for your Python version.
+.. code-block:: bash
-Python 2.7:
+ cd tutorial
-.. code-block:: text
+On Windows
+^^^^^^^^^^
- c:\> c:\Python27\Scripts\virtualenv %VENV%
+.. code-block:: doscon
-Python 3.2:
+ cd tutorial
-.. code-block:: text
- c:\> c:\Python32\Scripts\virtualenv %VENV%
+Set and use a ``VENV`` environment variable
+-------------------------------------------
-Install Pyramid Into the Virtual Python Environment
----------------------------------------------------
+We will set the ``VENV`` environment variable to the absolute path of the virtual environment, and use it going forward.
-On UNIX
+On Unix
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/easy_install pyramid
+ export VENV=~/tutorial
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
- c:\env> %VENV%\Scripts\easy_install pyramid
+ set VENV=c:\tutorial
-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`..
+Create a virtual environment
+----------------------------
-If you need to install the SQLite3 packages, then, for example, using
-the Debian system and apt-get, the command would be the following:
+On Unix
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ sudo apt-get install libsqlite3-dev
+ python3 -m venv $VENV
-Change Directory to Your Virtual Python Environment
----------------------------------------------------
+On Windows
+^^^^^^^^^^
-Change directory to the ``pyramidtut`` directory.
+Each version of Python uses different paths, so you will need to adjust the path to the command for your Python version. Recent versions of the Python 3 installer for Windows now install a Python launcher.
-On UNIX
+Python 2.7:
+
+.. code-block:: doscon
+
+ c:\Python27\Scripts\virtualenv %VENV%
+
+Python 3.7:
+
+.. code-block:: doscon
+
+ python -m venv %VENV%
+
+
+Upgrade packaging tools in the virtual environment
+--------------------------------------------------
+
+On Unix
^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ cd pyramidtut
+ $VENV/bin/pip install --upgrade pip setuptools
On Windows
^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
- c:\> cd pyramidtut
+ %VENV%\Scripts\pip install --upgrade pip setuptools
-.. _sql_making_a_project:
-Making a Project
-================
+.. _installing_project_in_dev_mode:
-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`.
+Installing the project in development mode
+------------------------------------------
-: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.
+In order to do development on the project easily, you must "register" the project as a development egg in your workspace. We will install testing requirements at the same time. We do so with the following command.
-By passing in `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.
+On Unix
+^^^^^^^
-The below instructions assume your current working directory is the
-"virtualenv" named "pyramidtut".
+.. code-block:: bash
-On UNIX
--------
+ $VENV/bin/pip install -e ".[testing]"
-.. code-block:: text
+On Windows
+^^^^^^^^^^
- $ $VENV/bin/pcreate -s alchemy tutorial
+.. code-block:: doscon
-On Windows
-----------
+ %VENV%\Scripts\pip install -e ".[testing]"
-.. code-block:: text
+On all operating systems
+^^^^^^^^^^^^^^^^^^^^^^^^
- c:\pyramidtut> %VENV%\pcreate -s alchemy tutorial
+The console will show ``pip`` checking for packages and installing missing packages. Success executing this command will show a line like the following:
-.. 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.
+.. code-block:: bash
-.. _installing_project_in_dev_mode:
+ Successfully installed Jinja2-2.8 Mako-1.0.6 MarkupSafe-0.23 \
+ PasteDeploy-1.5.2 Pygments-2.1.3 SQLAlchemy-1.1.4 WebOb-1.6.3 \
+ WebTest-2.0.24 beautifulsoup4-4.5.1 coverage-4.2 py-1.4.32 pyramid-1.7.3 \
+ pyramid-debugtoolbar-3.0.5 pyramid-jinja2-2.7 pyramid-mako-1.0.2 \
+ pyramid-tm-1.1.1 pytest-3.0.5 pytest-cov-2.4.0 repoze.lru-0.6 six-1.10.0 \
+ transaction-2.0.3 translationstring-1.3 tutorial venusian-1.0 \
+ waitress-1.0.1 zope.deprecation-4.2.0 zope.interface-4.3.3 \
+ zope.sqlalchemy-0.7.7
-Installing the Project in Development Mode
-==========================================
+Testing requirements are defined in our project's ``setup.py`` file, in the ``tests_require`` and ``extras_require`` stanzas.
-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.
+.. literalinclude:: src/installation/setup.py
+ :language: python
+ :lineno-match:
+ :lines: 24-28
-On UNIX
--------
+.. literalinclude:: src/installation/setup.py
+ :language: python
+ :lineno-match:
+ :lines: 48-50
-.. code-block:: text
- $ cd tutorial
- $ $VENV/bin/python setup.py develop
+.. _initialize_db_wiki2:
+
+Initialize and upgrade the database using Alembic
+-------------------------------------------------
+
+We use :term:`Alembic` to manage our database initialization and migrations.
+
+Generate your first revision.
+
+On Unix
+^^^^^^^
+
+.. code-block:: bash
+
+ $VENV/bin/alembic -c development.ini revision --autogenerate -m "init"
On Windows
-----------
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ %VENV%\Scripts\alembic -c development.ini revision --autogenerate -m "init"
+
+The output to your console should be something like this:
.. code-block:: text
- c:\pyramidtut> cd tutorial
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+ 2018-06-22 17:57:31,587 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:31,587 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:31,588 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:31,588 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:31,589 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-22 17:57:31,589 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:31,590 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-22 17:57:31,590 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:31,590 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ CREATE TABLE alembic_version (
+ version_num VARCHAR(32) NOT NULL,
+ CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
+ )
+
+
+ 2018-06-22 17:57:31,591 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:31,591 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-22 17:57:31,594 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT name FROM sqlite_master WHERE type='table' ORDER BY name
+ 2018-06-22 17:57:31,594 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ Generating /<somepath>/tutorial/alembic/versions/20180622_bab5a278ce04.py ... done
+
+Upgrade to that revision.
+
+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
- Finished processing dependencies for tutorial==0.0
+ $VENV/bin/alembic -c development.ini upgrade head
-.. _sql_running_tests:
+On Windows
+^^^^^^^^^^
-Running the Tests
-=================
+.. code-block:: doscon
-After you've installed the project in development mode, you may run
-the tests for the project.
+ %VENV%\Scripts\alembic -c development.ini upgrade head
-On UNIX
--------
+The output to your console should be something like this:
.. code-block:: text
- $ $VENV/bin/python setup.py test -q
+ 2018-06-22 17:57:37,814 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:37,814 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:37,814 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:37,814 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:37,816 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-22 17:57:37,816 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,817 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT alembic_version.version_num
+ FROM alembic_version
+ 2018-06-22 17:57:37,817 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,817 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-22 17:57:37,817 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,819 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ CREATE TABLE models (
+ id INTEGER NOT NULL,
+ name TEXT,
+ value INTEGER,
+ CONSTRAINT pk_models PRIMARY KEY (id)
+ )
+
+
+ 2018-06-22 17:57:37,820 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,822 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-22 17:57:37,824 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] CREATE UNIQUE INDEX my_index ON models (name)
+ 2018-06-22 17:57:37,824 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,825 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-22 17:57:37,825 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO alembic_version (version_num) VALUES ('bab5a278ce04')
+ 2018-06-22 17:57:37,825 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,825 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+
+
+.. _load_data_wiki2:
+
+Load default data
+-----------------
+
+Load default data into the database using a :term:`console script`. 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:: bash
+
+ $VENV/bin/initialize_tutorial_db development.ini
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
+ %VENV%\Scripts\initialize_tutorial_db development.ini
-For a successful test run, you should see output that ends like this::
+The output to your console should be something like this:
- .
- ----------------------------------------------------------------------
- Ran 1 test in 0.094s
-
- OK
+.. code-block:: bash
-Exposing Test Coverage Information
-==================================
+ 2018-06-22 17:57:46,241 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:46,241 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:46,242 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:46,242 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:46,243 INFO [sqlalchemy.engine.base.Engine:682][MainThread] BEGIN (implicit)
+ 2018-06-22 17:57:46,244 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO models (name, value) VALUES (?, ?)
+ 2018-06-22 17:57:46,245 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('one', 1)
+ 2018-06-22 17:57:46,246 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
-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
-tests.
+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``) and single record inside of that.
-To get this functionality working, we'll need to install the ``nose`` and
-``coverage`` packages into our ``virtualenv``:
-On UNIX
--------
+.. _sql_running_tests:
-.. code-block:: text
+Run the tests
+-------------
+
+After you've installed the project in development mode as well as the testing
+requirements, you may run the tests for the project. The following commands
+provide options to ``pytest`` that specify the module for which its tests shall be
+run, and to run ``pytest`` in quiet mode.
- $ $VENV/bin/easy_install nose coverage
+On Unix
+^^^^^^^
+
+.. code-block:: bash
+
+ $VENV/bin/pytest -q
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\easy_install nose coverage
+ %VENV%\Scripts\pytest -q
-Once ``nose`` and ``coverage`` are installed, we can actually run the
-coverage tests.
+For a successful test run, you should see output that ends like this:
-On UNIX
--------
+.. code-block:: bash
-.. code-block:: text
+ ..
+ 2 passed in 0.44 seconds
- $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage
+
+Expose test coverage information
+--------------------------------
+
+You can run the ``pytest`` command to see test coverage information. This
+runs the tests in the same way that ``pytest`` does, but provides additional
+:term:`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:: bash
+
+ $VENV/bin/pytest --cov --cov-report=term-missing
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \
- --cover-erase --with-coverage
+ c:\tutorial> %VENV%\Scripts\pytest --cov --cov-report=term-missing
-If successful, you will see output something like this::
+If successful, you will see output something like this:
- .
- Name Stmts Miss Cover Missing
- ------------------------------------------------
- tutorial 11 7 36% 9-15
- tutorial.models 17 0 100%
- tutorial.scripts 0 0 100%
- tutorial.tests 24 0 100%
- tutorial.views 6 0 100%
- ------------------------------------------------
- TOTAL 58 7 88%
- ----------------------------------------------------------------------
- Ran 1 test in 0.459s
+.. code-block:: bash
- OK
+ ======================== test session starts ========================
+ platform Python 3.6.5, pytest-3.6.2, py-1.5.3, pluggy-0.6.0
+ rootdir: /<somepath>/tutorial, inifile: pytest.ini
+ plugins: cov-2.5.1
+ collected 2 items
-Looks like our package doesn't quite have 100% test coverage.
+ tutorial/tests.py ..
+ ------------------ coverage: platform Python 3.6.5 ------------------
+ Name Stmts Miss Cover Missing
+ -----------------------------------------------------------------
+ tutorial/__init__.py 8 6 25% 7-12
+ tutorial/models/__init__.py 24 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/initialize_db.py 24 24 0% 1-34
+ tutorial/views/__init__.py 0 0 100%
+ tutorial/views/default.py 12 0 100%
+ tutorial/views/notfound.py 4 4 0% 1-7
+ -----------------------------------------------------------------
+ TOTAL 88 37 58%
+ ===================== 2 passed in 0.57 seconds ======================
-.. _initialize_db_wiki2:
+Our package doesn't quite have 100% test coverage.
-Initializing the Database
-=========================
-We need to use the ``initialize_tutorial_db`` :term:`console
-script` to initialize our database.
+.. _test_and_coverage_cookiecutter_defaults_sql:
-Type the following command, make sure you are still in the ``tutorial``
-directory (the directory with a ``development.ini`` in it):
+Test and coverage cookiecutter defaults
+---------------------------------------
-On UNIX
--------
+Cookiecutters include configuration defaults for ``pytest`` and test coverage.
+These configuration files are ``pytest.ini`` and ``.coveragerc``, located at
+the root of your package. Without these defaults, we would need to specify the
+path to the module on which we want to run tests and coverage.
-.. code-block:: text
+On Unix
+^^^^^^^
- $ $VENV/bin/initialize_tutorial_db development.ini
+.. code-block:: bash
+
+ $VENV/bin/pytest --cov=tutorial tutorial/tests.py -q
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
+
+ %VENV%\Scripts\pytest --cov=tutorial tutorial\tests.py -q
+
+pytest follows :ref:`conventions for Python test discovery
+<pytest:test discovery>`, and the configuration defaults from the cookiecutter
+tell ``pytest`` where to find the module on which we want to run tests and
+coverage.
+
+.. seealso:: See ``pytest``'s documentation for :ref:`pytest:usage` or invoke
+ ``pytest -h`` to see its full set of options.
- c:\pyramidtut\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini
-
-The output to your console should be something like this::
-
- 2011-11-26 14:42:25,012 INFO [sqlalchemy.engine.base.Engine][MainThread]
- PRAGMA table_info("models")
- 2011-11-26 14:42:25,013 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2011-11-26 14:42:25,013 INFO [sqlalchemy.engine.base.Engine][MainThread]
- CREATE TABLE models (
- id INTEGER NOT NULL,
- name VARCHAR(255),
- value INTEGER,
- PRIMARY KEY (id),
- UNIQUE (name)
- )
- 2011-11-26 14:42:25,013 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2011-11-26 14:42:25,135 INFO [sqlalchemy.engine.base.Engine][MainThread]
- COMMIT
- 2011-11-26 14:42:25,137 INFO [sqlalchemy.engine.base.Engine][MainThread]
- BEGIN (implicit)
- 2011-11-26 14:42:25,138 INFO [sqlalchemy.engine.base.Engine][MainThread]
- INSERT INTO models (name, value) VALUES (?, ?)
- 2011-11-26 14:42:25,139 INFO [sqlalchemy.engine.base.Engine][MainThread]
- (u'one', 1)
- 2011-11-26 14:42:25,140 INFO [sqlalchemy.engine.base.Engine][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
-(``models``).
.. _wiki2-start-the-application:
-Starting the Application
-========================
+Start the application
+---------------------
-Start the application.
+Start the application. See :ref:`what_is_this_pserve_thing` for more
+information on ``pserve``.
-On UNIX
--------
+On Unix
+^^^^^^^
-.. code-block:: text
+.. code-block:: bash
- $ $VENV/bin/pserve development.ini --reload
+ $VENV/bin/pserve development.ini --reload
On Windows
-----------
+^^^^^^^^^^
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload
+ %VENV%\Scripts\pserve development.ini --reload
-If successful, you will see something like this on your console::
+.. note::
- Starting subprocess with file monitor
- Starting server in PID 8966.
- Starting HTTP server on http://0.0.0.0:6543
+ 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:
+
+.. code-block:: text
+
+ Starting subprocess with file monitor
+ Starting server in PID 44078.
+ Serving on http://localhost:6543
+ Serving on http://localhost:6543
This means the server is ready to accept requests.
-At this point, when you visit ``http://localhost:6543/`` in your web browser,
-you will see the generated application's default page.
+
+Visit the application in a browser
+----------------------------------
+
+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:
+Decisions the cookiecutter backend option ``sqlalchemy`` has made for you
+-------------------------------------------------------------------------
+
+When creating a project and selecting the backend option of ``sqlalchemy``, the
+cookiecutter makes the following assumptions:
-- you are willing to use :term:`SQLAlchemy` as a database access tool
+- You are willing to use SQLite for persistent storage, although almost any SQL database could be used with SQLAlchemy.
-- you are willing to use :term:`url dispatch` to map URLs to code.
+- You are willing to use :term:`SQLAlchemy` for a database access tool.
-- you want to use ``ZopeTransactionExtension`` and ``pyramid_tm`` to scope
- sessions to requests
+- You are willing to use :term:`Alembic` for a database migrations tool.
+
+- You are willing to use a :term:`console script` for a data loading tool.
+
+- You are willing to use :term:`URL dispatch` to map URLs to code.
+
+- You want to use zope.sqlalchemy_, pyramid_tm_, and the transaction_ packages
+ to scope sessions to requests.
.. note::
- :app:`Pyramid` supports any persistent storage mechanism (e.g. object
- database or filesystem files, etc). 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.
+ :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 :term:`URL dispatch` and :term:`SQLAlchemy`.
+
+.. _pyramid_jinja2:
+ https://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/
+
+.. _pyramid_tm:
+ https://docs.pylonsproject.org/projects/pyramid-tm/en/latest/
+
+.. _zope.sqlalchemy:
+ https://pypi.org/project/zope.sqlalchemy/
+
+.. _transaction:
+ https://zodb.readthedocs.io/en/latest/transactions.html
diff --git a/docs/tutorials/wiki2/src/authentication/.coveragerc b/docs/tutorials/wiki2/src/authentication/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki2/src/authentication/.gitignore b/docs/tutorials/wiki2/src/authentication/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/authentication/CHANGES.txt b/docs/tutorials/wiki2/src/authentication/CHANGES.txt
new file mode 100644
index 000000000..14b902fd1
--- /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..05cc195d9
--- /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 *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt
new file mode 100644
index 000000000..5d5133e34
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/README.txt
@@ -0,0 +1,43 @@
+myproj
+======
+
+Getting Started
+---------------
+
+- Change directory into your newly created project.
+
+ cd tutorial
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
+
+ env/bin/initialize_tutorial_db development.ini
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/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..8fbb5fd38
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/development.ini
@@ -0,0 +1,82 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+retry.attempts = 3
+
+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
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_tutorial]
+level = DEBUG
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = 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/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini
new file mode 100644
index 000000000..9fef64f83
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/production.ini
@@ -0,0 +1,76 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+retry.attempts = 3
+
+auth.secret = real-seekrit
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
+[server:main]
+use = egg:waitress#main
+listen = *:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_tutorial]
+level = WARN
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/authentication/pytest.ini b/docs/tutorials/wiki2/src/authentication/pytest.ini
new file mode 100644
index 000000000..a3489cdf8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py
new file mode 100644
index 000000000..e2a30c0e7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/setup.py
@@ -0,0 +1,63 @@
+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 = [
+ 'alembic',
+ 'bcrypt',
+ 'docutils',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9',
+ 'pyramid_debugtoolbar',
+ 'pyramid_jinja2',
+ 'pyramid_retry',
+ 'pyramid_tm',
+ 'SQLAlchemy',
+ 'transaction',
+ 'zope.sqlalchemy',
+ 'waitress',
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ 'pytest-cov',
+]
+
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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.initialize_db: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..5d4bae3d7
--- /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.
+ """
+ with Configurator(settings=settings) as config:
+ 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/alembic/env.py b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
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..a4209a6e9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
@@ -0,0 +1,78 @@
+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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ # use pyramid_retry to retry a request when transient exceptions occur
+ config.include('pyramid_retry')
+
+ 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..02285b3ff
--- /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.zzzcomputing.com/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..74ff1faf8
--- /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(Text, 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..9228b48f7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
@@ -0,0 +1,28 @@
+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.decode('utf8')
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash.encode('utf8')
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
+ return False
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/pshell.py b/docs/tutorials/wiki2/src/authentication/tutorial/pshell.py
new file mode 100644
index 000000000..108c04d5e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/pshell.py
@@ -0,0 +1,12 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['dbsession'] = request.dbsession
+ env['models'] = models
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/initialize_db.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..e6350fb36
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import argparse
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def parse_args(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'config_uri',
+ help='Configuration file, e.g., development.ini',
+ )
+ return parser.parse_args(argv[1:])
+
+
+def main(argv=sys.argv):
+ args = parse_args(argv)
+ setup_logging(args.config_uri)
+ env = bootstrap(args.config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+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 initialize your database tables with `alembic`.
+ Check your README.txt for description 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.
+ ''')
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/authentication/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..4016b26c9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="{{request.locale_name}}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></script>
+ <![endif]-->
+ </head>
+
+ <body>
+
+ <div class="starter-template">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-2">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png') }}" alt="pyramid web framework">
+ </div>
+ <div class="col-md-10">
+ <div class="content">
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
+</html>
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2
new file mode 100644
index 000000000..1806de0ff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2
@@ -0,0 +1,26 @@
+{% extends 'layout.jinja2' %}
+
+{% block title %}Login - {% endblock title %}
+
+{% block content %}
+<p>
+<strong>
+ Login
+</strong><br>
+{{ message }}
+</p>
+<form action="{{ url }}" method="post">
+<input type="hidden" name="next" value="{{ next_url }}">
+<div class="form-group">
+ <label for="login">Username</label>
+ <input type="text" name="login" value="{{ login }}">
+</div>
+<div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" name="password">
+</div>
+<div class="form-group">
+ <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+</div>
+</form>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2
new file mode 100644
index 000000000..94419e228
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2
@@ -0,0 +1,18 @@
+{% extends 'layout.jinja2' %}
+
+{% block subtitle %}{{page.name}} - {% endblock subtitle %}
+
+{% block content %}
+<p>{{ content|safe }}</p>
+<p>
+<a href="{{ edit_url }}">
+ Edit this page
+</a>
+</p>
+<p>
+ Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
+</p>
+<p>You can return to the
+<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
+</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/tests.py b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py
new file mode 100644
index 000000000..ce650ca7c
--- /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'], 'myproj')
+
+
+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..8ed90d5b2
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
@@ -0,0 +1,79 @@
+from pyramid.compat import escape
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import (
+ HTTPForbidden,
+ HTTPFound,
+ HTTPNotFound,
+ )
+
+from pyramid.view import view_config
+
+from .. import models
+
+# 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(models.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(models.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, escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, 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(models.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(models.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 = models.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/.coveragerc b/docs/tutorials/wiki2/src/authorization/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki2/src/authorization/.gitignore b/docs/tutorials/wiki2/src/authorization/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/authorization/CHANGES.txt b/docs/tutorials/wiki2/src/authorization/CHANGES.txt
index 35a34f332..14b902fd1 100644
--- a/docs/tutorials/wiki2/src/authorization/CHANGES.txt
+++ b/docs/tutorials/wiki2/src/authorization/CHANGES.txt
@@ -1,4 +1,4 @@
0.0
---
-- Initial version
+- Initial version.
diff --git a/docs/tutorials/wiki2/src/authorization/MANIFEST.in b/docs/tutorials/wiki2/src/authorization/MANIFEST.in
index 81beba1b1..05cc195d9 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 *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt
index 68f430110..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/authorization/README.txt
+++ b/docs/tutorials/wiki2/src/authorization/README.txt
@@ -1,14 +1,43 @@
-tutorial README
-==================
+myproj
+======
Getting Started
---------------
-- cd <directory containing this file>
+- Change directory into your newly created project.
-- $VENV/bin/python setup.py develop
+ cd tutorial
-- $VENV/bin/initialize_tutorial_db development.ini
+- Create a Python virtual environment.
-- $VENV/bin/pserve development.ini
+ python3 -m venv env
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
+
+ env/bin/initialize_tutorial_db development.ini
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini
index a9d53b296..8fbb5fd38 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,26 +13,37 @@ pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_debugtoolbar
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+retry.attempts = 3
+
+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
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -54,7 +65,7 @@ handlers =
qualname = tutorial
[logger_sqlalchemy]
-level = INFO
+level = WARN
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
@@ -68,4 +79,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..9fef64f83 100644
--- a/docs/tutorials/wiki2/src/authorization/production.ini
+++ b/docs/tutorials/wiki2/src/authorization/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,17 +11,34 @@ 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
+retry.attempts = 3
+
+auth.secret = real-seekrit
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
-# Begin logging configuration
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +73,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/pytest.ini b/docs/tutorials/wiki2/src/authorization/pytest.ini
new file mode 100644
index 000000000..a3489cdf8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py
index 09bd63d33..e2a30c0e7 100644
--- a/docs/tutorials/wiki2/src/authorization/setup.py
+++ b/docs/tutorials/wiki2/src/authorization/setup.py
@@ -9,40 +9,55 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
- 'pyramid_chameleon',
+ 'alembic',
+ 'bcrypt',
+ 'docutils',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
+ 'pyramid_jinja2',
+ 'pyramid_retry',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
- ]
+]
-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",
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ 'pytest-cov',
+]
+
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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.initialize_db:main',
],
- author='',
- author_email='',
- url='',
- keywords='web wsgi bfg pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- test_suite='tutorial',
- 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/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
index 2ada42171..5d4bae3d7 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.scan()
+ with Configurator(settings=settings) as config:
+ 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/alembic/env.py b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
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..a4209a6e9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
@@ -0,0 +1,78 @@
+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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ # use pyramid_retry to retry a request when transient exceptions occur
+ config.include('pyramid_retry')
+
+ 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..02285b3ff
--- /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.zzzcomputing.com/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..74ff1faf8
--- /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(Text, 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..9228b48f7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py
@@ -0,0 +1,28 @@
+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.decode('utf8')
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash.encode('utf8')
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
+ return False
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/pshell.py b/docs/tutorials/wiki2/src/authorization/tutorial/pshell.py
new file mode 100644
index 000000000..108c04d5e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/pshell.py
@@ -0,0 +1,12 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['dbsession'] = request.dbsession
+ env['models'] = models
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..1fd45a994
--- /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 . import models
+
+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(models.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(models.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/initialize_db.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..e6350fb36
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import argparse
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def parse_args(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'config_uri',
+ help='Configuration file, e.g., development.ini',
+ )
+ return parser.parse_args(argv[1:])
+
+
+def main(argv=sys.argv):
+ args = parse_args(argv)
+ setup_logging(args.config_uri)
+ env = bootstrap(args.config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+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 initialize your database tables with `alembic`.
+ Check your README.txt for description 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.
+ ''')
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
deleted file mode 100644
index 23a5f13f4..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import os
-import sys
-import transaction
-
-from sqlalchemy import engine_from_config
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from ..models import (
- DBSession,
- Page,
- Base,
- )
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) != 2:
- usage(argv)
- config_uri = argv[1]
- setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
index d88c9c71f..1ce1c8753 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 . import models
+
+
+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(models.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/favicon.ico b/docs/tutorials/wiki2/src/authorization/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/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/tutorials/wiki2/src/authorization/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/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/tutorials/wiki2/src/authorization/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/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/static/transparent.gif b/docs/tutorials/wiki2/src/authorization/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
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 ca28b9fa5..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt
+++ /dev/null
@@ -1,62 +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>${page.name} - 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">
- Editing <b><span tal:replace="page.name">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <form action="${save_url}" method="post">
- <textarea name="body" tal:content="page.data" rows="10"
- cols="60"/><br/>
- <input type="submit" name="form.submitted" value="Save"/>
- </form>
- </div>
- </div>
- </div>
- <div id="footer">
- <div class="footer"
- >&copy; Copyright 2008-2011, Agendaless Consulting.</div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..4016b26c9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="{{request.locale_name}}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></script>
+ <![endif]-->
+ </head>
+
+ <body>
+
+ <div class="starter-template">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-2">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png') }}" alt="pyramid web framework">
+ </div>
+ <div class="col-md-10">
+ <div class="content">
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
+</html>
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 64e592ea9..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt
+++ /dev/null
@@ -1,58 +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>
- <div id="footer">
- <div class="footer"
- >&copy; Copyright 2008-2011, Agendaless Consulting.</div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt
deleted file mode 100644
index cf3da2073..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,76 +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>The Pyramid Web Framework</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="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <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">
- <div class="top align-center">
- <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
- </div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
- <div id="footer">
- <div class="footer">&copy; Copyright 2008-2011, Agendaless Consulting.</div>
- </div>
-</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/templates/view.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt
deleted file mode 100644
index 5a69818c1..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt
+++ /dev/null
@@ -1,65 +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>${page.name} - 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">
- Viewing <b><span tal:replace="page.name">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- </div>
- </div>
- </div>
- <div id="footer">
- <div class="footer"
- >&copy; Copyright 2008-2011, Agendaless Consulting.</div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
index 9f01d2da5..ce650ca7c 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'], 'myproj')
+
+
+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..ad271fb46
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
@@ -0,0 +1,64 @@
+from pyramid.compat import escape
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+from .. import models
+
+# 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(models.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, escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, 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 = models.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/.coveragerc b/docs/tutorials/wiki2/src/basiclayout/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki2/src/basiclayout/.gitignore b/docs/tutorials/wiki2/src/basiclayout/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt b/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt
index 35a34f332..14b902fd1 100644
--- a/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt
+++ b/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt
@@ -1,4 +1,4 @@
0.0
---
-- Initial version
+- Initial version.
diff --git a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in
index 81beba1b1..05cc195d9 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 *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt
index 68f430110..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/basiclayout/README.txt
+++ b/docs/tutorials/wiki2/src/basiclayout/README.txt
@@ -1,14 +1,43 @@
-tutorial README
-==================
+myproj
+======
Getting Started
---------------
-- cd <directory containing this file>
+- Change directory into your newly created project.
-- $VENV/bin/python setup.py develop
+ cd tutorial
-- $VENV/bin/initialize_tutorial_db development.ini
+- Create a Python virtual environment.
-- $VENV/bin/pserve development.ini
+ python3 -m venv env
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
+
+ env/bin/initialize_tutorial_db development.ini
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini
index a9d53b296..564aefb56 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,26 +13,35 @@ pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_debugtoolbar
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+retry.attempts = 3
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -54,7 +63,7 @@ handlers =
qualname = tutorial
[logger_sqlalchemy]
-level = INFO
+level = WARN
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
@@ -68,4 +77,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..29cdda1e1 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -11,19 +11,31 @@ 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
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -59,4 +71,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/pytest.ini b/docs/tutorials/wiki2/src/basiclayout/pytest.ini
new file mode 100644
index 000000000..a3489cdf8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py
index 15e7e5923..11725dd51 100644
--- a/docs/tutorials/wiki2/src/basiclayout/setup.py
+++ b/docs/tutorials/wiki2/src/basiclayout/setup.py
@@ -9,39 +9,53 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
- 'pyramid_chameleon',
+ 'alembic',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
+ 'pyramid_jinja2',
+ 'pyramid_retry',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- ]
+]
-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",
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ 'pytest-cov',
+]
+
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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.initialize_db:main',
],
- author='',
- author_email='',
- url='',
- keywords='web wsgi bfg pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- test_suite='tutorial',
- 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/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
index 867049e4f..28bd1f80d 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.scan()
+ with Configurator(settings=settings) as config:
+ 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/alembic/env.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
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 0cdd4bbc3..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py
+++ /dev/null
@@ -1,24 +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 MyModel(Base):
- __tablename__ = 'models'
- id = Column(Integer, primary_key=True)
- name = Column(Text, unique=True)
- value = Column(Integer)
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..d8a273e9e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
@@ -0,0 +1,77 @@
+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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ # use pyramid_retry to retry a request when transient exceptions occur
+ config.include('pyramid_retry')
+
+ 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..02285b3ff
--- /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.zzzcomputing.com/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/pshell.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/pshell.py
new file mode 100644
index 000000000..108c04d5e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/pshell.py
@@ -0,0 +1,12 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['dbsession'] = request.dbsession
+ env['models'] = models
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py
new file mode 100644
index 000000000..25504ad4d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py
@@ -0,0 +1,3 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..c629d1780
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initialize_db.py
@@ -0,0 +1,48 @@
+import argparse
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ """
+ Add or update models / fixtures in the database.
+
+ """
+ model = models.mymodel.MyModel(name='one', value=1)
+ dbsession.add(model)
+
+
+def parse_args(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'config_uri',
+ help='Configuration file, e.g., development.ini',
+ )
+ return parser.parse_args(argv[1:])
+
+
+def main(argv=sys.argv):
+ args = parse_args(argv)
+ setup_logging(args.config_uri)
+ env = bootstrap(args.config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+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 initialize your database tables with `alembic`.
+ Check your README.txt for description 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.
+ ''')
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
deleted file mode 100644
index 66feb3008..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import os
-import sys
-import transaction
-
-from sqlalchemy import engine_from_config
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from ..models import (
- DBSession,
- MyModel,
- Base,
- )
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) != 2:
- usage(argv)
- config_uri = argv[1]
- setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = MyModel(name='one', value=1)
- DBSession.add(model)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/favicon.ico b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/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/tutorials/wiki2/src/basiclayout/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/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/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/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/basiclayout/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
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/basiclayout/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..5d4313fe2
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="{{request.locale_name}}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>Cookiecutter Alchemy project for the Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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><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="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://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="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
+</html>
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..d8b0a4232
--- /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 project</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
+</div>
+{% endblock content %} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt
deleted file mode 100644
index ca4e0af26..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,73 +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>The Pyramid Web Framework</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" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <!--[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">
- <div class="top align-center">
- <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
- </div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py
index 57a775e0a..ce650ca7c 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')
+ self.assertEqual(info['project'], 'myproj')
+
+
+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/models/tutorial/views.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
index 4cfcae4af..ef69ff895 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
@@ -3,27 +3,25 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from .models import (
- DBSession,
- MyModel,
- )
+from .. import models
-@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(models.MyModel)
+ one = query.filter(models.MyModel.name == 'one').first()
except DBAPIError:
- return Response(conn_err_msg, content_type='text/plain', status_int=500)
- return {'one': one, 'project': 'tutorial'}
+ return Response(db_err_msg, content_type='text/plain', status=500)
+ return {'one': one, 'project': 'myproj'}
-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
- environment's "bin" directory for this script and try to run it.
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description 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
@@ -32,4 +30,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/.coveragerc b/docs/tutorials/wiki2/src/installation/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki2/src/installation/.gitignore b/docs/tutorials/wiki2/src/installation/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/installation/CHANGES.txt b/docs/tutorials/wiki2/src/installation/CHANGES.txt
new file mode 100644
index 000000000..14b902fd1
--- /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..05cc195d9
--- /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 *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/docs/tutorials/wiki2/src/installation/README.txt b/docs/tutorials/wiki2/src/installation/README.txt
new file mode 100644
index 000000000..5d5133e34
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/README.txt
@@ -0,0 +1,43 @@
+myproj
+======
+
+Getting Started
+---------------
+
+- Change directory into your newly created project.
+
+ cd tutorial
+
+- Create a Python virtual environment.
+
+ python3 -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
+
+ env/bin/initialize_tutorial_db development.ini
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/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..564aefb56
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/development.ini
@@ -0,0 +1,80 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+retry.attempts = 3
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_tutorial]
+level = DEBUG
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = 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/production.ini b/docs/tutorials/wiki2/src/installation/production.ini
new file mode 100644
index 000000000..29cdda1e1
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/production.ini
@@ -0,0 +1,74 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
+[server:main]
+use = egg:waitress#main
+listen = *:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_tutorial]
+level = WARN
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki2/src/installation/pytest.ini b/docs/tutorials/wiki2/src/installation/pytest.ini
new file mode 100644
index 000000000..a3489cdf8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/installation/setup.py b/docs/tutorials/wiki2/src/installation/setup.py
new file mode 100644
index 000000000..11725dd51
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/setup.py
@@ -0,0 +1,61 @@
+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 = [
+ 'alembic',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9',
+ 'pyramid_debugtoolbar',
+ 'pyramid_jinja2',
+ 'pyramid_retry',
+ 'pyramid_tm',
+ 'SQLAlchemy',
+ 'transaction',
+ 'zope.sqlalchemy',
+ 'waitress',
+]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ 'pytest-cov',
+]
+
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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.initialize_db: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..28bd1f80d
--- /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.
+ """
+ with Configurator(settings=settings) as config:
+ 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/alembic/env.py b/docs/tutorials/wiki2/src/installation/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/installation/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
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..d8a273e9e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
@@ -0,0 +1,77 @@
+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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ # use pyramid_retry to retry a request when transient exceptions occur
+ config.include('pyramid_retry')
+
+ 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..02285b3ff
--- /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.zzzcomputing.com/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/pshell.py b/docs/tutorials/wiki2/src/installation/tutorial/pshell.py
new file mode 100644
index 000000000..108c04d5e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/pshell.py
@@ -0,0 +1,12 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['dbsession'] = request.dbsession
+ env['models'] = models
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/initialize_db.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..c629d1780
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initialize_db.py
@@ -0,0 +1,48 @@
+import argparse
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ """
+ Add or update models / fixtures in the database.
+
+ """
+ model = models.mymodel.MyModel(name='one', value=1)
+ dbsession.add(model)
+
+
+def parse_args(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'config_uri',
+ help='Configuration file, e.g., development.ini',
+ )
+ return parser.parse_args(argv[1:])
+
+
+def main(argv=sys.argv):
+ args = parse_args(argv)
+ setup_logging(args.config_uri)
+ env = bootstrap(args.config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+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 initialize your database tables with `alembic`.
+ Check your README.txt for description 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.
+ ''')
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/installation/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..5d4313fe2
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="{{request.locale_name}}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>Cookiecutter Alchemy project for the Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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><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="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://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="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
+</html>
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..d8b0a4232
--- /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 project</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
+</div>
+{% endblock content %} \ No newline at end of file
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..ce650ca7c
--- /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'], 'myproj')
+
+
+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/basiclayout/tutorial/views.py b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
index 4cfcae4af..ef69ff895 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
@@ -3,27 +3,25 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from .models import (
- DBSession,
- MyModel,
- )
+from .. import models
-@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(models.MyModel)
+ one = query.filter(models.MyModel.name == 'one').first()
except DBAPIError:
- return Response(conn_err_msg, content_type='text/plain', status_int=500)
- return {'one': one, 'project': 'tutorial'}
+ return Response(db_err_msg, content_type='text/plain', status=500)
+ return {'one': one, 'project': 'myproj'}
-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
- environment's "bin" directory for this script and try to run it.
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description 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
@@ -32,4 +30,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/.coveragerc b/docs/tutorials/wiki2/src/models/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki2/src/models/.gitignore b/docs/tutorials/wiki2/src/models/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/models/CHANGES.txt b/docs/tutorials/wiki2/src/models/CHANGES.txt
index 35a34f332..14b902fd1 100644
--- a/docs/tutorials/wiki2/src/models/CHANGES.txt
+++ b/docs/tutorials/wiki2/src/models/CHANGES.txt
@@ -1,4 +1,4 @@
0.0
---
-- Initial version
+- Initial version.
diff --git a/docs/tutorials/wiki2/src/models/MANIFEST.in b/docs/tutorials/wiki2/src/models/MANIFEST.in
index 81beba1b1..05cc195d9 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 *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt
index 68f430110..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/models/README.txt
+++ b/docs/tutorials/wiki2/src/models/README.txt
@@ -1,14 +1,43 @@
-tutorial README
-==================
+myproj
+======
Getting Started
---------------
-- cd <directory containing this file>
+- Change directory into your newly created project.
-- $VENV/bin/python setup.py develop
+ cd tutorial
-- $VENV/bin/initialize_tutorial_db development.ini
+- Create a Python virtual environment.
-- $VENV/bin/pserve development.ini
+ python3 -m venv env
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
+
+ env/bin/initialize_tutorial_db development.ini
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini
index a9d53b296..564aefb56 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,26 +13,35 @@ pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_debugtoolbar
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+retry.attempts = 3
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -54,7 +63,7 @@ handlers =
qualname = tutorial
[logger_sqlalchemy]
-level = INFO
+level = WARN
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
@@ -68,4 +77,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..29cdda1e1 100644
--- a/docs/tutorials/wiki2/src/models/production.ini
+++ b/docs/tutorials/wiki2/src/models/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,17 +11,32 @@ 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
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
-# Begin logging configuration
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +71,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/pytest.ini b/docs/tutorials/wiki2/src/models/pytest.ini
new file mode 100644
index 000000000..a3489cdf8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py
index 15e7e5923..09e3126ea 100644
--- a/docs/tutorials/wiki2/src/models/setup.py
+++ b/docs/tutorials/wiki2/src/models/setup.py
@@ -9,39 +9,54 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
- 'pyramid_chameleon',
+ 'alembic',
+ 'bcrypt',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
+ 'pyramid_jinja2',
+ 'pyramid_retry',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- ]
+]
-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",
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ 'pytest-cov',
+]
+
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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.initialize_db:main',
],
- author='',
- author_email='',
- url='',
- keywords='web wsgi bfg pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- test_suite='tutorial',
- 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/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
index 867049e4f..28bd1f80d 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.scan()
+ with Configurator(settings=settings) as config:
+ 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/alembic/env.py b/docs/tutorials/wiki2/src/models/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/models/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/models/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
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..a4209a6e9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
@@ -0,0 +1,78 @@
+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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ # use pyramid_retry to retry a request when transient exceptions occur
+ config.include('pyramid_retry')
+
+ 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..02285b3ff
--- /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.zzzcomputing.com/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..74ff1faf8
--- /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(Text, 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..9228b48f7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py
@@ -0,0 +1,28 @@
+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.decode('utf8')
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash.encode('utf8')
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
+ return False
diff --git a/docs/tutorials/wiki2/src/models/tutorial/pshell.py b/docs/tutorials/wiki2/src/models/tutorial/pshell.py
new file mode 100644
index 000000000..108c04d5e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/pshell.py
@@ -0,0 +1,12 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['dbsession'] = request.dbsession
+ env['models'] = models
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/initialize_db.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..e6350fb36
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import argparse
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def parse_args(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'config_uri',
+ help='Configuration file, e.g., development.ini',
+ )
+ return parser.parse_args(argv[1:])
+
+
+def main(argv=sys.argv):
+ args = parse_args(argv)
+ setup_logging(args.config_uri)
+ env = bootstrap(args.config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+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 initialize your database tables with `alembic`.
+ Check your README.txt for description 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.
+ ''')
diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
deleted file mode 100644
index 23a5f13f4..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import os
-import sys
-import transaction
-
-from sqlalchemy import engine_from_config
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from ..models import (
- DBSession,
- Page,
- Base,
- )
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) != 2:
- usage(argv)
- config_uri = argv[1]
- setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/favicon.ico b/docs/tutorials/wiki2/src/models/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/models/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/models/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/models/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/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/tutorials/wiki2/src/models/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/models/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/models/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/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/tutorials/wiki2/src/models/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/theme.css b/docs/tutorials/wiki2/src/models/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/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/models/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/models/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
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..5d4313fe2
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="{{request.locale_name}}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>Cookiecutter Alchemy project for the Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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><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="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://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="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></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..d8b0a4232
--- /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 project</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
+</div>
+{% endblock content %} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt
deleted file mode 100644
index ca4e0af26..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,73 +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>The Pyramid Web Framework</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" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <!--[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">
- <div class="top align-center">
- <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
- </div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py
index 57a775e0a..ce650ca7c 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')
+ self.assertEqual(info['project'], 'myproj')
+
+
+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..ef69ff895
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
@@ -0,0 +1,32 @@
+from pyramid.response import Response
+from pyramid.view import view_config
+
+from sqlalchemy.exc import DBAPIError
+
+from .. import models
+
+
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
+def my_view(request):
+ try:
+ query = request.dbsession.query(models.MyModel)
+ one = query.filter(models.MyModel.name == 'one').first()
+ except DBAPIError:
+ return Response(db_err_msg, content_type='text/plain', status=500)
+ return {'one': one, 'project': 'myproj'}
+
+
+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 initialize your database tables with `alembic`.
+ Check your README.txt for description 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/.coveragerc b/docs/tutorials/wiki2/src/tests/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki2/src/tests/.gitignore b/docs/tutorials/wiki2/src/tests/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/tests/CHANGES.txt b/docs/tutorials/wiki2/src/tests/CHANGES.txt
index 35a34f332..14b902fd1 100644
--- a/docs/tutorials/wiki2/src/tests/CHANGES.txt
+++ b/docs/tutorials/wiki2/src/tests/CHANGES.txt
@@ -1,4 +1,4 @@
0.0
---
-- Initial version
+- Initial version.
diff --git a/docs/tutorials/wiki2/src/tests/MANIFEST.in b/docs/tutorials/wiki2/src/tests/MANIFEST.in
index 81beba1b1..05cc195d9 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 *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt
index 68f430110..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/tests/README.txt
+++ b/docs/tutorials/wiki2/src/tests/README.txt
@@ -1,14 +1,43 @@
-tutorial README
-==================
+myproj
+======
Getting Started
---------------
-- cd <directory containing this file>
+- Change directory into your newly created project.
-- $VENV/bin/python setup.py develop
+ cd tutorial
-- $VENV/bin/initialize_tutorial_db development.ini
+- Create a Python virtual environment.
-- $VENV/bin/pserve development.ini
+ python3 -m venv env
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
+
+ env/bin/initialize_tutorial_db development.ini
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini
index a9d53b296..8fbb5fd38 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,26 +13,37 @@ pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_debugtoolbar
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+retry.attempts = 3
+
+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
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -54,7 +65,7 @@ handlers =
qualname = tutorial
[logger_sqlalchemy]
-level = INFO
+level = WARN
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
@@ -68,4 +79,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..9fef64f83 100644
--- a/docs/tutorials/wiki2/src/tests/production.ini
+++ b/docs/tutorials/wiki2/src/tests/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,17 +11,34 @@ 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
+retry.attempts = 3
+
+auth.secret = real-seekrit
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
-# Begin logging configuration
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +73,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/pytest.ini b/docs/tutorials/wiki2/src/tests/pytest.ini
new file mode 100644
index 000000000..a3489cdf8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py
index d8486e462..e2a30c0e7 100644
--- a/docs/tutorials/wiki2/src/tests/setup.py
+++ b/docs/tutorials/wiki2/src/tests/setup.py
@@ -9,41 +9,55 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
- 'pyramid_chameleon',
+ 'alembic',
+ 'bcrypt',
+ 'docutils',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
+ 'pyramid_jinja2',
+ 'pyramid_retry',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
- 'WebTest', # add this
- ]
+]
-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",
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ 'pytest-cov',
+]
+
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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.initialize_db:main',
],
- author='',
- author_email='',
- url='',
- keywords='web wsgi bfg pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- test_suite='tutorial',
- 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/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py
index cee89184b..5d4bae3d7 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.scan()
+ with Configurator(settings=settings) as config:
+ 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/alembic/env.py b/docs/tutorials/wiki2/src/tests/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/tests/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
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..a4209a6e9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
@@ -0,0 +1,78 @@
+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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ # use pyramid_retry to retry a request when transient exceptions occur
+ config.include('pyramid_retry')
+
+ 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..02285b3ff
--- /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.zzzcomputing.com/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..74ff1faf8
--- /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(Text, 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..9228b48f7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py
@@ -0,0 +1,28 @@
+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.decode('utf8')
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash.encode('utf8')
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
+ return False
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/pshell.py b/docs/tutorials/wiki2/src/tests/tutorial/pshell.py
new file mode 100644
index 000000000..108c04d5e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/pshell.py
@@ -0,0 +1,12 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['dbsession'] = request.dbsession
+ env['models'] = models
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..1fd45a994
--- /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 . import models
+
+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(models.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(models.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/initialize_db.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..e6350fb36
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import argparse
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def parse_args(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'config_uri',
+ help='Configuration file, e.g., development.ini',
+ )
+ return parser.parse_args(argv[1:])
+
+
+def main(argv=sys.argv):
+ args = parse_args(argv)
+ setup_logging(args.config_uri)
+ env = bootstrap(args.config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+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 initialize your database tables with `alembic`.
+ Check your README.txt for description 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.
+ ''')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
deleted file mode 100644
index 23a5f13f4..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import os
-import sys
-import transaction
-
-from sqlalchemy import engine_from_config
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from ..models import (
- DBSession,
- Page,
- Base,
- )
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) != 2:
- usage(argv)
- config_uri = argv[1]
- setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py
index d88c9c71f..1ce1c8753 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 . import models
+
+
+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(models.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/favicon.ico b/docs/tutorials/wiki2/src/tests/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/tests/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/tests/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/tests/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/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/tutorials/wiki2/src/tests/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/tests/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/tests/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/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/tutorials/wiki2/src/tests/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.css b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/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/tests/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/tests/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
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 2004273fe..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt
+++ /dev/null
@@ -1,58 +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>${page.name} - 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">
- Editing <b><span tal:replace="page.name">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <form action="${save_url}" method="post">
- <textarea name="body" tal:content="page.data" rows="10"
- cols="60"/><br/>
- <input type="submit" name="form.submitted" value="Save"/>
- </form>
- </div>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..4016b26c9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="{{request.locale_name}}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></script>
+ <![endif]-->
+ </head>
+
+ <body>
+
+ <div class="starter-template">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-2">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png') }}" alt="pyramid web framework">
+ </div>
+ <div class="col-md-10">
+ <div class="content">
+ {% if request.user is none %}
+ <p class="pull-right">
+ <a href="{{ request.route_url('login') }}">Login</a>
+ </p>
+ {% else %}
+ <p class="pull-right">
+ {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
+ </p>
+ {% endif %}
+ {% block content %}{% endblock %}
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
+</html>
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/mytemplate.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt
deleted file mode 100644
index 6c1ca924a..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,73 +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>The Pyramid Web Framework</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="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <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">
- <div class="top align-center">
- <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
- </div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
- </ul>
- </div>
- </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/templates/view.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt
deleted file mode 100644
index 19c50fb36..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt
+++ /dev/null
@@ -1,61 +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>${page.name} - 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">
- Viewing <b><span tal:replace="page.name">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right">
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
- </div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- </div>
- </div>
- </div>
-</body>
-</html>
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..0250e71c9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
@@ -0,0 +1,134 @@
+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')
+ basic_login_no_next = (
+ '/login?login=basic&password=basic'
+ '&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_successful_log_in_no_next(self):
+ res = self.testapp.get(self.basic_login_no_next, status=302)
+ self.assertEqual(res.location, 'http://localhost/')
+
+ 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)
+
+ def test_redirect_to_edit_for_existing_page(self):
+ self.testapp.get(self.editor_login, status=302)
+ res = self.testapp.get('/add_page/FrontPage', status=302)
+ self.assertTrue(b'FrontPage' in res.body)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py
new file mode 100644
index 000000000..72fbff04b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py
@@ -0,0 +1,16 @@
+import os
+import unittest
+
+
+class TestInitializeDB(unittest.TestCase):
+
+ def test_usage(self):
+ from ..scripts.initialize_db import main
+ with self.assertRaises(SystemExit):
+ main(argv=['foo'])
+
+ def test_run(self):
+ from ..scripts.initialize_db import main
+ main(argv=['foo', 'development.ini'])
+ self.assertTrue(os.path.exists('tutorial.sqlite'))
+ os.remove('tutorial.sqlite')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py
new file mode 100644
index 000000000..cbec6420d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py
@@ -0,0 +1,23 @@
+import unittest
+from pyramid.testing import DummyRequest
+
+
+class TestMyAuthenticationPolicy(unittest.TestCase):
+
+ def test_no_user(self):
+ request = DummyRequest()
+ request.user = None
+
+ from ..security import MyAuthenticationPolicy
+ policy = MyAuthenticationPolicy(None)
+ self.assertEqual(policy.authenticated_userid(request), None)
+
+ def test_authenticated_user(self):
+ from ..models import User
+ request = DummyRequest()
+ request.user = User()
+ request.user.id = 'foo'
+
+ from ..security import MyAuthenticationPolicy
+ policy = MyAuthenticationPolicy(None)
+ self.assertEqual(policy.authenticated_userid(request), 'foo')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py
new file mode 100644
index 000000000..9490ac990
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py
@@ -0,0 +1,67 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+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):
+ from ..models import User
+ return User(name=name, role=role)
+
+
+class TestSetPassword(BaseTest):
+
+ def test_password_hash_saved(self):
+ user = self.makeUser(name='foo', role='bar')
+ self.assertFalse(user.password_hash)
+
+ user.set_password('secret')
+ self.assertTrue(user.password_hash)
+
+
+class TestCheckPassword(BaseTest):
+
+ def test_password_hash_not_set(self):
+ user = self.makeUser(name='foo', role='bar')
+ self.assertFalse(user.password_hash)
+
+ self.assertFalse(user.check_password('secret'))
+
+ def test_correct_password(self):
+ user = self.makeUser(name='foo', role='bar')
+ user.set_password('secret')
+ self.assertTrue(user.password_hash)
+
+ self.assertTrue(user.check_password('secret'))
+
+ def test_incorrect_password(self):
+ user = self.makeUser(name='foo', role='bar')
+ user.set_password('secret')
+ self.assertTrue(user.password_hash)
+
+ self.assertFalse(user.check_password('incorrect'))
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..ad271fb46
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
@@ -0,0 +1,64 @@
+from pyramid.compat import escape
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+from .. import models
+
+# 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(models.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, escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, 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 = models.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/.coveragerc b/docs/tutorials/wiki2/src/views/.coveragerc
new file mode 100644
index 000000000..a1d87d03d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = tutorial
+omit = tutorial/test*
diff --git a/docs/tutorials/wiki2/src/views/.gitignore b/docs/tutorials/wiki2/src/views/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/views/CHANGES.txt b/docs/tutorials/wiki2/src/views/CHANGES.txt
index 35a34f332..14b902fd1 100644
--- a/docs/tutorials/wiki2/src/views/CHANGES.txt
+++ b/docs/tutorials/wiki2/src/views/CHANGES.txt
@@ -1,4 +1,4 @@
0.0
---
-- Initial version
+- Initial version.
diff --git a/docs/tutorials/wiki2/src/views/MANIFEST.in b/docs/tutorials/wiki2/src/views/MANIFEST.in
index 81beba1b1..05cc195d9 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 *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt
index 68f430110..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/views/README.txt
+++ b/docs/tutorials/wiki2/src/views/README.txt
@@ -1,14 +1,43 @@
-tutorial README
-==================
+myproj
+======
Getting Started
---------------
-- cd <directory containing this file>
+- Change directory into your newly created project.
-- $VENV/bin/python setup.py develop
+ cd tutorial
-- $VENV/bin/initialize_tutorial_db development.ini
+- Create a Python virtual environment.
-- $VENV/bin/pserve development.ini
+ python3 -m venv env
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
+
+ env/bin/initialize_tutorial_db development.ini
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini
index a9d53b296..564aefb56 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
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
@@ -13,26 +13,35 @@ pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_debugtoolbar
- pyramid_tm
sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite
+retry.attempts = 3
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
+[pshell]
+setup = tutorial.pshell.setup
+
###
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = localhost:6543
###
# logging configuration
-# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
@@ -54,7 +63,7 @@ handlers =
qualname = tutorial
[logger_sqlalchemy]
-level = INFO
+level = WARN
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
@@ -68,4 +77,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..29cdda1e1 100644
--- a/docs/tutorials/wiki2/src/views/production.ini
+++ b/docs/tutorials/wiki2/src/views/production.ini
@@ -1,3 +1,8 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
[app:main]
use = egg:tutorial
@@ -6,17 +11,32 @@ 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
+retry.attempts = 3
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
-host = 0.0.0.0
-port = 6543
+listen = *:6543
-# Begin logging configuration
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
[loggers]
keys = root, tutorial, sqlalchemy
@@ -51,6 +71,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/pytest.ini b/docs/tutorials/wiki2/src/views/pytest.ini
new file mode 100644
index 000000000..a3489cdf8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tutorial
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py
index 09bd63d33..e2a30c0e7 100644
--- a/docs/tutorials/wiki2/src/views/setup.py
+++ b/docs/tutorials/wiki2/src/views/setup.py
@@ -9,40 +9,55 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
- 'pyramid',
- 'pyramid_chameleon',
+ 'alembic',
+ 'bcrypt',
+ 'docutils',
+ 'plaster_pastedeploy',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
+ 'pyramid_jinja2',
+ 'pyramid_retry',
'pyramid_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'waitress',
- 'docutils',
- ]
+]
-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",
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest>=3.7.4',
+ 'pytest-cov',
+]
+
+setup(
+ name='tutorial',
+ version='0.0',
+ description='myproj',
+ 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 pyramid pylons',
+ 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.initialize_db:main',
],
- author='',
- author_email='',
- url='',
- keywords='web wsgi bfg pylons pyramid',
- packages=find_packages(),
- include_package_data=True,
- zip_safe=False,
- test_suite='tutorial',
- 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/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
index 37cae1997..28bd1f80d 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.scan()
+ with Configurator(settings=settings) as config:
+ 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/alembic/env.py b/docs/tutorials/wiki2/src/views/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/views/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/views/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
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..a4209a6e9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
@@ -0,0 +1,78 @@
+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()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ # use pyramid_retry to retry a request when transient exceptions occur
+ config.include('pyramid_retry')
+
+ 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..02285b3ff
--- /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.zzzcomputing.com/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..74ff1faf8
--- /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(Text, 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..9228b48f7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py
@@ -0,0 +1,28 @@
+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.decode('utf8')
+
+ def check_password(self, pw):
+ if self.password_hash is not None:
+ expected_hash = self.password_hash.encode('utf8')
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
+ return False
diff --git a/docs/tutorials/wiki2/src/views/tutorial/pshell.py b/docs/tutorials/wiki2/src/views/tutorial/pshell.py
new file mode 100644
index 000000000..108c04d5e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/pshell.py
@@ -0,0 +1,12 @@
+from . import models
+
+def setup(env):
+ request = env['request']
+
+ # start a transaction
+ request.tm.begin()
+
+ # inject some vars into the shell builtins
+ env['tm'] = request.tm
+ env['dbsession'] = request.dbsession
+ env['models'] = models
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/initialize_db.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..e6350fb36
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import argparse
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def parse_args(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ 'config_uri',
+ help='Configuration file, e.g., development.ini',
+ )
+ return parser.parse_args(argv[1:])
+
+
+def main(argv=sys.argv):
+ args = parse_args(argv)
+ setup_logging(args.config_uri)
+ env = bootstrap(args.config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+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 initialize your database tables with `alembic`.
+ Check your README.txt for description 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.
+ ''')
diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
deleted file mode 100644
index 23a5f13f4..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import os
-import sys
-import transaction
-
-from sqlalchemy import engine_from_config
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from ..models import (
- DBSession,
- Page,
- Base,
- )
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri>\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) != 2:
- usage(argv)
- config_uri = argv[1]
- setup_logging(config_uri)
- settings = get_appsettings(config_uri)
- engine = engine_from_config(settings, 'sqlalchemy.')
- DBSession.configure(bind=engine)
- Base.metadata.create_all(engine)
- with transaction.manager:
- model = Page(name='FrontPage', data='This is the front page')
- DBSession.add(model)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/favicon.ico b/docs/tutorials/wiki2/src/views/tutorial/static/favicon.ico
deleted file mode 100644
index 71f837c9e..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/views/tutorial/static/footerbg.png
deleted file mode 100644
index 1fbc873da..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/static/footerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/views/tutorial/static/headerbg.png
deleted file mode 100644
index 0596f2020..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/static/headerbg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/views/tutorial/static/ie6.css
deleted file mode 100644
index b7c8493d8..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/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/tutorials/wiki2/src/views/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/views/tutorial/static/middlebg.png
deleted file mode 100644
index 2369cfb7d..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/static/middlebg.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/views/tutorial/static/pylons.css
deleted file mode 100644
index 4b1c017cd..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/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/tutorials/wiki2/src/views/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-16x16.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-small.png
deleted file mode 100644
index a5bc0ade7..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-small.png
+++ /dev/null
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid.png
index 347e05549..4ab837be9 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid.png
+++ b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid.png
Binary files differ
diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/theme.css b/docs/tutorials/wiki2/src/views/tutorial/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/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/views/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/views/tutorial/static/transparent.gif
deleted file mode 100644
index 0341802e5..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/static/transparent.gif
+++ /dev/null
Binary files differ
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/views/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt
deleted file mode 100644
index 5f962bbf5..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.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>${page.name} - 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">
- Editing <b><span tal:replace="page.name">Page Name Goes
- Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right"></div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <form action="${save_url}" method="post">
- <textarea name="body" tal:content="page.data" rows="10"
- cols="60"/><br/>
- <input type="submit" name="form.submitted" value="Save"/>
- </form>
- </div>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
new file mode 100644
index 000000000..80062cbff
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="{{request.locale_name}}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
+
+ <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
+
+ <!-- Bootstrap core CSS -->
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
+
+ <!-- HTML5 shiv 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" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></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">
+ {% block content %}{% endblock %}
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+ </body>
+</html>
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 ca4e0af26..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt
+++ /dev/null
@@ -1,73 +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>The Pyramid Web Framework</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" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" />
- <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" />
- <!--[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">
- <div class="top align-center">
- <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div>
- </div>
- </div>
- <div id="middle">
- <div class="middle align-center">
- <p class="app-welcome">
- Welcome to <span class="app-name">${project}</span>, an application generated by<br/>
- the Pyramid Web Framework.
- </p>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div id="left" class="align-right">
- <h2>Search documentation</h2>
- <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html">
- <input type="text" id="q" name="q" value="" />
- <input type="submit" id="x" value="Go" />
- </form>
- </div>
- <div id="right" class="align-left">
- <h2>Pyramid links</h2>
- <ul class="links">
- <li>
- <a href="http://pylonsproject.org">Pylons Website</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a>
- </li>
- <li>
- <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a>
- </li>
- <li>
- <a href="irc://irc.freenode.net#pyramid">IRC Channel</a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
-</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/templates/view.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt
deleted file mode 100644
index 78c0d2d4c..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt
+++ /dev/null
@@ -1,57 +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>${page.name} - 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">
- Viewing <b><span tal:replace="page.name">Page Name
- Goes Here</span></b><br/>
- You can return to the
- <a href="${request.application_url}">FrontPage</a>.<br/>
- </div>
- <div id="right" class="app-welcome align-right"></div>
- </div>
- </div>
- <div id="bottom">
- <div class="bottom">
- <div tal:replace="structure content">
- Page text goes here.
- </div>
- <p>
- <a tal:attributes="href edit_url" href="">
- Edit this page
- </a>
- </p>
- </div>
- </div>
- </div>
-</body>
-</html>
diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py
index 9f01d2da5..ce650ca7c 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'], 'myproj')
+
+
+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 b41d4ab40..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/views.py
+++ /dev/null
@@ -1,71 +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..a866af1de
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
@@ -0,0 +1,73 @@
+from pyramid.compat import escape
+import re
+from docutils.core import publish_parts
+
+from pyramid.httpexceptions import (
+ HTTPFound,
+ HTTPNotFound,
+ )
+
+from pyramid.view import view_config
+
+from .. import models
+
+# 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(models.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(models.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, escape(word))
+ else:
+ add_url = request.route_url('add_page', pagename=word)
+ return '<a href="%s">%s</a>' % (add_url, 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(models.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(models.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 = models.Page(name=pagename, data=body)
+ page.creator = (
+ request.dbsession.query(models.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 9aca0c5b7..941a50928 100644
--- a/docs/tutorials/wiki2/tests.rst
+++ b/docs/tutorials/wiki2/tests.rst
@@ -1,100 +1,118 @@
+.. _wiki2_adding_tests:
+
============
Adding Tests
============
-We will now add tests for the models and the views and a few functional
-tests in the ``tests.py``. Tests ensure that an application works, and
-that it continues to work after 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 from choosing the ``sqlalchemy`` backend
+option, 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``.
-Testing the Models
-==================
+.. warning::
-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.
+ 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!
-Testing the Views
-=================
-We'll modify our ``tests.py`` file, adding tests for each view
-function we added above. As a result, we'll *delete* the
-``ViewTests`` class that the ``alchemy`` scaffold provided, and add
-four other test classes: ``ViewWikiTests``, ``ViewPageTests``,
-``AddPageTests``, and ``EditPageTests``. These test the
-``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` views
-respectively.
+Test the views
+==============
+
+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.
-Viewing the results of all our edits to ``tests.py``
-====================================================
-Once we're done with the ``tests.py`` module, it will look a lot like:
+View the results of all our edits to ``tests`` subpackage
+=========================================================
-.. literalinclude:: src/tests/tutorial/tests.py
- :linenos:
- :language: python
+Create ``tutorial/tests/test_views.py`` such that it appears as follows:
-Running the Tests
-=================
+.. literalinclude:: src/tests/tutorial/tests/test_views.py
+ :linenos:
+ :language: python
-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``.
+Create ``tutorial/tests/test_functional.py`` such that it appears as follows:
-.. literalinclude:: src/tests/setup.py
- :linenos:
- :language: python
- :lines: 11-22
- :emphasize-lines: 11
+.. literalinclude:: src/tests/tutorial/tests/test_functional.py
+ :linenos:
+ :language: python
-After we've added a dependency on WebTest in ``setup.py``, we need to rerun
-``setup.py develop`` to get WebTest installed into our virtualenv. Assuming
-our shell's current working directory is the "tutorial" distribution
-directory:
+Create ``tutorial/tests/test_initdb.py`` such that it appears as follows:
-On UNIX:
+.. literalinclude:: src/tests/tutorial/tests/test_initdb.py
+ :linenos:
+ :language: python
-.. code-block:: text
+Create ``tutorial/tests/test_security.py`` such that it appears as follows:
- $ $VENV/bin/python setup.py develop
+.. literalinclude:: src/tests/tutorial/tests/test_security.py
+ :linenos:
+ :language: python
-On Windows:
+Create ``tutorial/tests/test_user_model.py`` such that it appears as follows:
-.. code-block:: text
+.. literalinclude:: src/tests/tutorial/tests/test_user_model.py
+ :linenos:
+ :language: python
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+.. note::
-Once that command has completed successfully, we can run the tests
-themselves:
+ 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 UNIX:
-.. code-block:: text
+Running the tests
+=================
+
+We can run these tests similarly to how we did in :ref:`running_tests`, but first delete the SQLite database ``tutorial.sqlite``. If you do not delete the database, then you will see an integrity error when running the tests.
- $ $VENV/bin/python setup.py test -q
+On Unix:
+
+.. code-block:: bash
+
+ rm tutorial.sqlite
+ $VENV/bin/pytest -q
On Windows:
-.. code-block:: text
+.. code-block:: doscon
- c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q
+ del tutorial.sqlite
+ %VENV%\Scripts\pytest -q
-The expected result ends something like:
+The expected result should look like the following:
.. code-block:: text
- ......................
- ----------------------------------------------------------------------
- Ran 21 tests in 2.700s
+ ................................
+ 32 passed in 9.90 seconds
- OK
+.. _webtest: https://docs.pylonsproject.org/projects/webtest/en/latest/