From a4b0781604fd217341cc43eec47a95c725860ced Mon Sep 17 00:00:00 2001
From: Michael Merickel
Date: Mon, 6 Jan 2020 22:57:34 -0600
Subject: sync basiclayout, installation, models with new structure
---
docs/tutorials/wiki2/basiclayout.rst | 14 +--
docs/tutorials/wiki2/definingmodels.rst | 10 +-
docs/tutorials/wiki2/installation.rst | 53 ++++-----
docs/tutorials/wiki2/src/basiclayout/.gitignore | 1 +
docs/tutorials/wiki2/src/basiclayout/testing.ini | 79 +++++++++++++
.../wiki2/src/basiclayout/tests/conftest.py | 125 +++++++++++++++++++++
.../wiki2/src/basiclayout/tests/test_functional.py | 13 +++
.../wiki2/src/basiclayout/tests/test_it.py | 66 -----------
.../wiki2/src/basiclayout/tests/test_views.py | 23 ++++
.../wiki2/src/basiclayout/tutorial/__init__.py | 2 +-
.../src/basiclayout/tutorial/models/__init__.py | 22 ++--
.../src/basiclayout/tutorial/views/default.py | 7 +-
docs/tutorials/wiki2/src/installation/.gitignore | 1 +
docs/tutorials/wiki2/src/installation/testing.ini | 79 +++++++++++++
.../wiki2/src/installation/tests/conftest.py | 125 +++++++++++++++++++++
.../src/installation/tests/test_functional.py | 13 +++
.../wiki2/src/installation/tests/test_it.py | 66 -----------
.../wiki2/src/installation/tests/test_views.py | 23 ++++
.../wiki2/src/installation/tutorial/__init__.py | 2 +-
.../src/installation/tutorial/models/__init__.py | 22 ++--
.../src/installation/tutorial/views/default.py | 7 +-
docs/tutorials/wiki2/src/models/.gitignore | 1 +
docs/tutorials/wiki2/src/models/setup.py | 2 +-
docs/tutorials/wiki2/src/models/testing.ini | 79 +++++++++++++
docs/tutorials/wiki2/src/models/tests/conftest.py | 125 +++++++++++++++++++++
.../wiki2/src/models/tests/test_functional.py | 13 +++
docs/tutorials/wiki2/src/models/tests/test_it.py | 66 -----------
.../tutorials/wiki2/src/models/tests/test_views.py | 23 ++++
.../wiki2/src/models/tutorial/__init__.py | 2 +-
.../wiki2/src/models/tutorial/models/__init__.py | 22 ++--
.../src/models/tutorial/scripts/initialize_db.py | 4 +
.../wiki2/src/models/tutorial/views/default.py | 7 +-
32 files changed, 827 insertions(+), 270 deletions(-)
create mode 100644 docs/tutorials/wiki2/src/basiclayout/testing.ini
create mode 100644 docs/tutorials/wiki2/src/basiclayout/tests/conftest.py
create mode 100644 docs/tutorials/wiki2/src/basiclayout/tests/test_functional.py
delete mode 100644 docs/tutorials/wiki2/src/basiclayout/tests/test_it.py
create mode 100644 docs/tutorials/wiki2/src/basiclayout/tests/test_views.py
create mode 100644 docs/tutorials/wiki2/src/installation/testing.ini
create mode 100644 docs/tutorials/wiki2/src/installation/tests/conftest.py
create mode 100644 docs/tutorials/wiki2/src/installation/tests/test_functional.py
delete mode 100644 docs/tutorials/wiki2/src/installation/tests/test_it.py
create mode 100644 docs/tutorials/wiki2/src/installation/tests/test_views.py
create mode 100644 docs/tutorials/wiki2/src/models/testing.ini
create mode 100644 docs/tutorials/wiki2/src/models/tests/conftest.py
create mode 100644 docs/tutorials/wiki2/src/models/tests/test_functional.py
delete mode 100644 docs/tutorials/wiki2/src/models/tests/test_it.py
create mode 100644 docs/tutorials/wiki2/src/models/tests/test_views.py
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst
index ae58d80a5..e8bc4c5a9 100644
--- a/docs/tutorials/wiki2/basiclayout.rst
+++ b/docs/tutorials/wiki2/basiclayout.rst
@@ -58,24 +58,24 @@ dictionary of settings parsed from the ``.ini`` file, which contains
deployment-related values, such as ``pyramid.reload_templates``,
``sqlalchemy.url``, and so on.
-Next include the package ``models`` using a dotted Python path. The exact
-setup of the models will be covered later.
+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: 8
:lineno-match:
:language: py
-Next include :term:`Jinja2` templating bindings so that we can use renderers
-with the ``.jinja2`` extension within our project.
+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: 9
:lineno-match:
:language: py
-Next include the ``routes`` module using a dotted Python path. This module will
-be explained in the next section.
+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: 10
@@ -207,7 +207,7 @@ Without repeating ourselves, we will point out the differences between this view
Content models with the ``models`` package
------------------------------------------
-In an SQLAlchemy-based application, a *model* object is an object composed by
+In a 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.
diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst
index 4b80e09ac..f84ca6588 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -32,8 +32,10 @@ parameter in the ``setup()`` function.
Open ``tutorial/setup.py`` and edit it to look like the following:
.. literalinclude:: src/models/setup.py
+ :lines: 11-30
:linenos:
- :emphasize-lines: 11-24
+ :lineno-match:
+ :emphasize-lines: 3
:language: python
It is a good practice to sort packages alphabetically to make them easier to find.
@@ -42,7 +44,9 @@ After adding ``bcrypt`` and sorting packages, we should have the above ``require
.. 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.
+ 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 .``
@@ -245,7 +249,7 @@ following:
.. literalinclude:: src/models/tutorial/scripts/initialize_db.py
:linenos:
:language: python
- :emphasize-lines: 11-24
+ :emphasize-lines: 15-28
Only the highlighted lines need to be changed.
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 55fca15a1..b144fc4e0 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -182,8 +182,8 @@ The console will show ``pip`` checking for packages and installing missing packa
alembic-1.3.2 attrs-19.3.0 beautifulsoup4-4.8.2 coverage-5.0.1 \
hupper-1.9.1 importlib-metadata-1.3.0 more-itertools-8.0.2 packaging-19.2 \
plaster-1.0 plaster-pastedeploy-0.7 pluggy-0.13.1 py-1.8.1 \
- pyparsing-2.4.6 pyramid-1.10.4 pyramid-debugtoolbar-4.5.1 \
- pyramid-jinja2-2.8 pyramid-mako-1.1.0 pyramid-retry-2.1 pyramid-tm-2.3 \
+ pyparsing-2.4.6 pyramid-1.10.4 pyramid-debugtoolbar-4.5.2 \
+ pyramid-jinja2-2.8 pyramid-mako-1.1.0 pyramid-retry-2.1 pyramid-tm-2.4 \
pytest-5.3.2 pytest-cov-2.8.1 python-dateutil-2.8.1 python-editor-1.0.4 \
repoze.lru-0.7 six-1.13.0 soupsieve-1.9.5 transaction-3.0.0 \
translationstring-1.3 tutorial venusian-3.0.0 waitress-1.4.1 \
@@ -350,30 +350,33 @@ If successful, you will see output something like this:
======================== test session starts ========================
platform -- Python 3.7.3, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
- rootdir: /tutorial, inifile: pytest.ini, testpaths: tutorial
+ rootdir: /tutorial, inifile: pytest.ini, testpaths: tutorial, tests
plugins: cov-2.8.1
- collected 2 items
-
- tutorial/tests.py ..
-
- ------------------ coverage: platform Python 3.7.3 ------------------
- 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/pshell.py 7 7 0% 1-13
- tutorial/routes.py 3 3 0% 1-3
- tutorial/scripts/__init__.py 0 0 100%
- tutorial/scripts/initialize_db.py 22 22 0% 1-38
- tutorial/views/__init__.py 0 0 100%
- tutorial/views/default.py 12 0 100%
- tutorial/views/notfound.py 4 4 0% 1-7
- -----------------------------------------------------------------
- TOTAL 93 42 55%
-
- ===================== 2 passed in 0.64 seconds ======================
+ collected 5 items
+
+ tests/test_functional.py ..
+ tests/test_views.py ...
+
+ ---------- coverage: platform darwin, python 3.7.4-final-0 -----------
+ Name Stmts Miss Cover Missing
+ ----------------------------------------------------------------------------------
+ tutorial/__init__.py 8 0 100%
+ tutorial/alembic/env.py 23 4 83% 28-30, 56
+ tutorial/alembic/versions/20200106_8c274fe5f3c4.py 12 2 83% 31-32
+ tutorial/models/__init__.py 32 2 94% 71, 82
+ tutorial/models/meta.py 5 0 100%
+ tutorial/models/mymodel.py 8 0 100%
+ tutorial/pshell.py 7 5 29% 5-13
+ tutorial/routes.py 3 0 100%
+ tutorial/scripts/__init__.py 0 0 100%
+ tutorial/scripts/initialize_db.py 22 14 36% 15-16, 20-25, 29-38
+ tutorial/views/__init__.py 0 0 100%
+ tutorial/views/default.py 12 0 100%
+ tutorial/views/notfound.py 4 0 100%
+ ----------------------------------------------------------------------------------
+ TOTAL 136 27 80%
+
+ ===================== 5 passed in 0.77 seconds ======================
Our package doesn't quite have 100% test coverage.
diff --git a/docs/tutorials/wiki2/src/basiclayout/.gitignore b/docs/tutorials/wiki2/src/basiclayout/.gitignore
index 1853d983c..c612e59f2 100644
--- a/docs/tutorials/wiki2/src/basiclayout/.gitignore
+++ b/docs/tutorials/wiki2/src/basiclayout/.gitignore
@@ -19,3 +19,4 @@ Data.fs*
.DS_Store
coverage
test
+*.sqlite
diff --git a/docs/tutorials/wiki2/src/basiclayout/testing.ini b/docs/tutorials/wiki2/src/basiclayout/testing.ini
new file mode 100644
index 000000000..85e5e1ae9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/testing.ini
@@ -0,0 +1,79 @@
+###
+# 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/testing.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, alembic
+
+[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.)
+
+[logger_alembic]
+level = WARN
+handlers =
+qualname = alembic
+
+[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/basiclayout/tests/conftest.py b/docs/tutorials/wiki2/src/basiclayout/tests/conftest.py
new file mode 100644
index 000000000..2db65f887
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tests/conftest.py
@@ -0,0 +1,125 @@
+import alembic
+import alembic.config
+import alembic.command
+import os
+from pyramid.paster import get_appsettings
+from pyramid.scripting import prepare
+from pyramid.testing import DummyRequest
+import pytest
+import transaction
+from webob.cookies import Cookie
+import webtest
+
+from tutorial import main
+from tutorial import models
+from tutorial.models.meta import Base
+
+
+def pytest_addoption(parser):
+ parser.addoption('--ini', action='store', metavar='INI_FILE')
+
+@pytest.fixture(scope='session')
+def ini_file(request):
+ # potentially grab this path from a pytest option
+ return os.path.abspath(request.config.option.ini or 'testing.ini')
+
+@pytest.fixture(scope='session')
+def app_settings(ini_file):
+ return get_appsettings(ini_file)
+
+@pytest.fixture(scope='session')
+def dbengine(app_settings, ini_file):
+ engine = models.get_engine(app_settings)
+
+ alembic_cfg = alembic.config.Config(ini_file)
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+ # run migrations to initialize the database
+ # depending on how we want to initialize the database from scratch
+ # we could alternatively call:
+ # Base.metadata.create_all(bind=engine)
+ # alembic.command.stamp(alembic_cfg, "head")
+ alembic.command.upgrade(alembic_cfg, "head")
+
+ yield engine
+
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+@pytest.fixture(scope='session')
+def app(app_settings, dbengine):
+ return main({}, dbengine=dbengine, **app_settings)
+
+@pytest.fixture
+def tm():
+ tm = transaction.TransactionManager(explicit=True)
+ tm.begin()
+ tm.doom()
+
+ yield tm
+
+ tm.abort()
+
+@pytest.fixture
+def dbsession(app, tm):
+ session_factory = app.registry['dbsession_factory']
+ return models.get_tm_session(session_factory, tm)
+
+@pytest.fixture
+def testapp(app, tm, dbsession):
+ # override request.dbsession and request.tm with our own
+ # externally-controlled values that are shared across requests but aborted
+ # at the end
+ testapp = webtest.TestApp(app, extra_environ={
+ 'HTTP_HOST': 'example.com',
+ 'tm.active': True,
+ 'tm.manager': tm,
+ 'app.dbsession': dbsession,
+ })
+
+ return testapp
+
+@pytest.fixture
+def app_request(app, tm, dbsession):
+ """
+ A real request.
+
+ This request is almost identical to a real request but it has some
+ drawbacks in tests as it's harder to mock data and is heavier.
+
+ """
+ env = prepare(registry=app.registry)
+ request = env['request']
+ request.host = 'example.com'
+
+ # without this, request.dbsession will be joined to the same transaction
+ # manager but it will be using a different sqlalchemy.orm.Session using
+ # a separate database transaction
+ request.dbsession = dbsession
+ request.tm = tm
+
+ yield request
+ env['closer']()
+
+@pytest.fixture
+def dummy_request(app, tm, dbsession):
+ """
+ A lightweight dummy request.
+
+ This request is ultra-lightweight and should be used only when the
+ request itself is not a large focus in the call-stack.
+
+ It is way easier to mock and control side-effects using this object.
+
+ - It does not have request extensions applied.
+ - Threadlocals are not properly pushed.
+
+ """
+ request = DummyRequest()
+ request.registry = app.registry
+ request.host = 'example.com'
+ request.dbsession = dbsession
+ request.tm = tm
+
+ return request
diff --git a/docs/tutorials/wiki2/src/basiclayout/tests/test_functional.py b/docs/tutorials/wiki2/src/basiclayout/tests/test_functional.py
new file mode 100644
index 000000000..dbcd8aec7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tests/test_functional.py
@@ -0,0 +1,13 @@
+from tutorial import models
+
+def test_my_view_success(testapp, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ res = testapp.get('/', status=200)
+ assert res.body
+
+def test_notfound(testapp):
+ res = testapp.get('/badurl', status=404)
+ assert res.status_code == 404
diff --git a/docs/tutorials/wiki2/src/basiclayout/tests/test_it.py b/docs/tutorials/wiki2/src/basiclayout/tests/test_it.py
deleted file mode 100644
index ea16534fc..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tests/test_it.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import unittest
-
-from pyramid import testing
-
-import transaction
-
-
-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('tutorial.models')
- settings = self.config.get_settings()
-
- from tutorial.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 tutorial.models.meta import Base
- Base.metadata.create_all(self.engine)
-
- def tearDown(self):
- from tutorial.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 tutorial.models import MyModel
-
- model = MyModel(name='one', value=55)
- self.session.add(model)
-
- def test_passing_view(self):
- from tutorial.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 tutorial.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/tests/test_views.py b/docs/tutorials/wiki2/src/basiclayout/tests/test_views.py
new file mode 100644
index 000000000..8ae464d03
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tests/test_views.py
@@ -0,0 +1,23 @@
+from tutorial import models
+from tutorial.views.default import my_view
+from tutorial.views.notfound import notfound_view
+
+
+def test_my_view_failure(app_request):
+ info = my_view(app_request)
+ assert info.status_int == 500
+
+def test_my_view_success(app_request, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ info = my_view(app_request)
+ assert app_request.response.status_int == 200
+ assert info['one'].name == 'one'
+ assert info['project'] == 'myproj'
+
+def test_notfound_view(app_request):
+ info = notfound_view(app_request)
+ assert app_request.response.status_int == 404
+ assert info == {}
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
index 5c2ba5cc0..7edc0957d 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py
@@ -5,8 +5,8 @@ def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
with Configurator(settings=settings) as config:
- config.include('.models')
config.include('pyramid_jinja2')
config.include('.routes')
+ config.include('.models')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
index d8a273e9e..1c3ec5ee8 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
@@ -65,13 +65,21 @@ def includeme(config):
# use pyramid_retry to retry a request when transient exceptions occur
config.include('pyramid_retry')
- session_factory = get_session_factory(get_engine(settings))
+ # hook to share the dbengine fixture in testing
+ dbengine = settings.get('dbengine')
+ if not dbengine:
+ dbengine = get_engine(settings)
+
+ session_factory = get_session_factory(dbengine)
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
- )
+ def dbsession(request):
+ # hook to share the dbsession fixture in testing
+ dbsession = request.environ.get('app.dbsession')
+ if dbsession is None:
+ # request.tm is the transaction manager used by pyramid_tm
+ dbsession = get_tm_session(session_factory, request.tm)
+ return dbsession
+
+ config.add_request_method(dbsession, reify=True)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
index 094b2f303..a0f654d38 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
@@ -1,7 +1,6 @@
from pyramid.view import view_config
from pyramid.response import Response
-
-from sqlalchemy.exc import DBAPIError
+from sqlalchemy.exc import SQLAlchemyError
from .. import models
@@ -10,8 +9,8 @@ from .. import models
def my_view(request):
try:
query = request.dbsession.query(models.MyModel)
- one = query.filter(models.MyModel.name == 'one').first()
- except DBAPIError:
+ one = query.filter(models.MyModel.name == 'one').one()
+ except SQLAlchemyError:
return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'myproj'}
diff --git a/docs/tutorials/wiki2/src/installation/.gitignore b/docs/tutorials/wiki2/src/installation/.gitignore
index 1853d983c..c612e59f2 100644
--- a/docs/tutorials/wiki2/src/installation/.gitignore
+++ b/docs/tutorials/wiki2/src/installation/.gitignore
@@ -19,3 +19,4 @@ Data.fs*
.DS_Store
coverage
test
+*.sqlite
diff --git a/docs/tutorials/wiki2/src/installation/testing.ini b/docs/tutorials/wiki2/src/installation/testing.ini
new file mode 100644
index 000000000..85e5e1ae9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/testing.ini
@@ -0,0 +1,79 @@
+###
+# 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/testing.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, alembic
+
+[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.)
+
+[logger_alembic]
+level = WARN
+handlers =
+qualname = alembic
+
+[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/tests/conftest.py b/docs/tutorials/wiki2/src/installation/tests/conftest.py
new file mode 100644
index 000000000..2db65f887
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tests/conftest.py
@@ -0,0 +1,125 @@
+import alembic
+import alembic.config
+import alembic.command
+import os
+from pyramid.paster import get_appsettings
+from pyramid.scripting import prepare
+from pyramid.testing import DummyRequest
+import pytest
+import transaction
+from webob.cookies import Cookie
+import webtest
+
+from tutorial import main
+from tutorial import models
+from tutorial.models.meta import Base
+
+
+def pytest_addoption(parser):
+ parser.addoption('--ini', action='store', metavar='INI_FILE')
+
+@pytest.fixture(scope='session')
+def ini_file(request):
+ # potentially grab this path from a pytest option
+ return os.path.abspath(request.config.option.ini or 'testing.ini')
+
+@pytest.fixture(scope='session')
+def app_settings(ini_file):
+ return get_appsettings(ini_file)
+
+@pytest.fixture(scope='session')
+def dbengine(app_settings, ini_file):
+ engine = models.get_engine(app_settings)
+
+ alembic_cfg = alembic.config.Config(ini_file)
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+ # run migrations to initialize the database
+ # depending on how we want to initialize the database from scratch
+ # we could alternatively call:
+ # Base.metadata.create_all(bind=engine)
+ # alembic.command.stamp(alembic_cfg, "head")
+ alembic.command.upgrade(alembic_cfg, "head")
+
+ yield engine
+
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+@pytest.fixture(scope='session')
+def app(app_settings, dbengine):
+ return main({}, dbengine=dbengine, **app_settings)
+
+@pytest.fixture
+def tm():
+ tm = transaction.TransactionManager(explicit=True)
+ tm.begin()
+ tm.doom()
+
+ yield tm
+
+ tm.abort()
+
+@pytest.fixture
+def dbsession(app, tm):
+ session_factory = app.registry['dbsession_factory']
+ return models.get_tm_session(session_factory, tm)
+
+@pytest.fixture
+def testapp(app, tm, dbsession):
+ # override request.dbsession and request.tm with our own
+ # externally-controlled values that are shared across requests but aborted
+ # at the end
+ testapp = webtest.TestApp(app, extra_environ={
+ 'HTTP_HOST': 'example.com',
+ 'tm.active': True,
+ 'tm.manager': tm,
+ 'app.dbsession': dbsession,
+ })
+
+ return testapp
+
+@pytest.fixture
+def app_request(app, tm, dbsession):
+ """
+ A real request.
+
+ This request is almost identical to a real request but it has some
+ drawbacks in tests as it's harder to mock data and is heavier.
+
+ """
+ env = prepare(registry=app.registry)
+ request = env['request']
+ request.host = 'example.com'
+
+ # without this, request.dbsession will be joined to the same transaction
+ # manager but it will be using a different sqlalchemy.orm.Session using
+ # a separate database transaction
+ request.dbsession = dbsession
+ request.tm = tm
+
+ yield request
+ env['closer']()
+
+@pytest.fixture
+def dummy_request(app, tm, dbsession):
+ """
+ A lightweight dummy request.
+
+ This request is ultra-lightweight and should be used only when the
+ request itself is not a large focus in the call-stack.
+
+ It is way easier to mock and control side-effects using this object.
+
+ - It does not have request extensions applied.
+ - Threadlocals are not properly pushed.
+
+ """
+ request = DummyRequest()
+ request.registry = app.registry
+ request.host = 'example.com'
+ request.dbsession = dbsession
+ request.tm = tm
+
+ return request
diff --git a/docs/tutorials/wiki2/src/installation/tests/test_functional.py b/docs/tutorials/wiki2/src/installation/tests/test_functional.py
new file mode 100644
index 000000000..dbcd8aec7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tests/test_functional.py
@@ -0,0 +1,13 @@
+from tutorial import models
+
+def test_my_view_success(testapp, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ res = testapp.get('/', status=200)
+ assert res.body
+
+def test_notfound(testapp):
+ res = testapp.get('/badurl', status=404)
+ assert res.status_code == 404
diff --git a/docs/tutorials/wiki2/src/installation/tests/test_it.py b/docs/tutorials/wiki2/src/installation/tests/test_it.py
deleted file mode 100644
index ea16534fc..000000000
--- a/docs/tutorials/wiki2/src/installation/tests/test_it.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import unittest
-
-from pyramid import testing
-
-import transaction
-
-
-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('tutorial.models')
- settings = self.config.get_settings()
-
- from tutorial.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 tutorial.models.meta import Base
- Base.metadata.create_all(self.engine)
-
- def tearDown(self):
- from tutorial.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 tutorial.models import MyModel
-
- model = MyModel(name='one', value=55)
- self.session.add(model)
-
- def test_passing_view(self):
- from tutorial.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 tutorial.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/tests/test_views.py b/docs/tutorials/wiki2/src/installation/tests/test_views.py
new file mode 100644
index 000000000..8ae464d03
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tests/test_views.py
@@ -0,0 +1,23 @@
+from tutorial import models
+from tutorial.views.default import my_view
+from tutorial.views.notfound import notfound_view
+
+
+def test_my_view_failure(app_request):
+ info = my_view(app_request)
+ assert info.status_int == 500
+
+def test_my_view_success(app_request, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ info = my_view(app_request)
+ assert app_request.response.status_int == 200
+ assert info['one'].name == 'one'
+ assert info['project'] == 'myproj'
+
+def test_notfound_view(app_request):
+ info = notfound_view(app_request)
+ assert app_request.response.status_int == 404
+ assert info == {}
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/__init__.py
index 5c2ba5cc0..7edc0957d 100644
--- a/docs/tutorials/wiki2/src/installation/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/installation/tutorial/__init__.py
@@ -5,8 +5,8 @@ def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
with Configurator(settings=settings) as config:
- config.include('.models')
config.include('pyramid_jinja2')
config.include('.routes')
+ config.include('.models')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
index d8a273e9e..1c3ec5ee8 100644
--- a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
@@ -65,13 +65,21 @@ def includeme(config):
# use pyramid_retry to retry a request when transient exceptions occur
config.include('pyramid_retry')
- session_factory = get_session_factory(get_engine(settings))
+ # hook to share the dbengine fixture in testing
+ dbengine = settings.get('dbengine')
+ if not dbengine:
+ dbengine = get_engine(settings)
+
+ session_factory = get_session_factory(dbengine)
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
- )
+ def dbsession(request):
+ # hook to share the dbsession fixture in testing
+ dbsession = request.environ.get('app.dbsession')
+ if dbsession is None:
+ # request.tm is the transaction manager used by pyramid_tm
+ dbsession = get_tm_session(session_factory, request.tm)
+ return dbsession
+
+ config.add_request_method(dbsession, reify=True)
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/default.py b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
index 094b2f303..a0f654d38 100644
--- a/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
@@ -1,7 +1,6 @@
from pyramid.view import view_config
from pyramid.response import Response
-
-from sqlalchemy.exc import DBAPIError
+from sqlalchemy.exc import SQLAlchemyError
from .. import models
@@ -10,8 +9,8 @@ from .. import models
def my_view(request):
try:
query = request.dbsession.query(models.MyModel)
- one = query.filter(models.MyModel.name == 'one').first()
- except DBAPIError:
+ one = query.filter(models.MyModel.name == 'one').one()
+ except SQLAlchemyError:
return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'myproj'}
diff --git a/docs/tutorials/wiki2/src/models/.gitignore b/docs/tutorials/wiki2/src/models/.gitignore
index 1853d983c..c612e59f2 100644
--- a/docs/tutorials/wiki2/src/models/.gitignore
+++ b/docs/tutorials/wiki2/src/models/.gitignore
@@ -19,3 +19,4 @@ Data.fs*
.DS_Store
coverage
test
+*.sqlite
diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py
index 60234751a..fbd848136 100644
--- a/docs/tutorials/wiki2/src/models/setup.py
+++ b/docs/tutorials/wiki2/src/models/setup.py
@@ -19,8 +19,8 @@ requires = [
'pyramid_tm',
'SQLAlchemy',
'transaction',
- 'zope.sqlalchemy',
'waitress',
+ 'zope.sqlalchemy',
]
tests_require = [
diff --git a/docs/tutorials/wiki2/src/models/testing.ini b/docs/tutorials/wiki2/src/models/testing.ini
new file mode 100644
index 000000000..85e5e1ae9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/testing.ini
@@ -0,0 +1,79 @@
+###
+# 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/testing.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, alembic
+
+[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.)
+
+[logger_alembic]
+level = WARN
+handlers =
+qualname = alembic
+
+[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/models/tests/conftest.py b/docs/tutorials/wiki2/src/models/tests/conftest.py
new file mode 100644
index 000000000..2db65f887
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tests/conftest.py
@@ -0,0 +1,125 @@
+import alembic
+import alembic.config
+import alembic.command
+import os
+from pyramid.paster import get_appsettings
+from pyramid.scripting import prepare
+from pyramid.testing import DummyRequest
+import pytest
+import transaction
+from webob.cookies import Cookie
+import webtest
+
+from tutorial import main
+from tutorial import models
+from tutorial.models.meta import Base
+
+
+def pytest_addoption(parser):
+ parser.addoption('--ini', action='store', metavar='INI_FILE')
+
+@pytest.fixture(scope='session')
+def ini_file(request):
+ # potentially grab this path from a pytest option
+ return os.path.abspath(request.config.option.ini or 'testing.ini')
+
+@pytest.fixture(scope='session')
+def app_settings(ini_file):
+ return get_appsettings(ini_file)
+
+@pytest.fixture(scope='session')
+def dbengine(app_settings, ini_file):
+ engine = models.get_engine(app_settings)
+
+ alembic_cfg = alembic.config.Config(ini_file)
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+ # run migrations to initialize the database
+ # depending on how we want to initialize the database from scratch
+ # we could alternatively call:
+ # Base.metadata.create_all(bind=engine)
+ # alembic.command.stamp(alembic_cfg, "head")
+ alembic.command.upgrade(alembic_cfg, "head")
+
+ yield engine
+
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+@pytest.fixture(scope='session')
+def app(app_settings, dbengine):
+ return main({}, dbengine=dbengine, **app_settings)
+
+@pytest.fixture
+def tm():
+ tm = transaction.TransactionManager(explicit=True)
+ tm.begin()
+ tm.doom()
+
+ yield tm
+
+ tm.abort()
+
+@pytest.fixture
+def dbsession(app, tm):
+ session_factory = app.registry['dbsession_factory']
+ return models.get_tm_session(session_factory, tm)
+
+@pytest.fixture
+def testapp(app, tm, dbsession):
+ # override request.dbsession and request.tm with our own
+ # externally-controlled values that are shared across requests but aborted
+ # at the end
+ testapp = webtest.TestApp(app, extra_environ={
+ 'HTTP_HOST': 'example.com',
+ 'tm.active': True,
+ 'tm.manager': tm,
+ 'app.dbsession': dbsession,
+ })
+
+ return testapp
+
+@pytest.fixture
+def app_request(app, tm, dbsession):
+ """
+ A real request.
+
+ This request is almost identical to a real request but it has some
+ drawbacks in tests as it's harder to mock data and is heavier.
+
+ """
+ env = prepare(registry=app.registry)
+ request = env['request']
+ request.host = 'example.com'
+
+ # without this, request.dbsession will be joined to the same transaction
+ # manager but it will be using a different sqlalchemy.orm.Session using
+ # a separate database transaction
+ request.dbsession = dbsession
+ request.tm = tm
+
+ yield request
+ env['closer']()
+
+@pytest.fixture
+def dummy_request(app, tm, dbsession):
+ """
+ A lightweight dummy request.
+
+ This request is ultra-lightweight and should be used only when the
+ request itself is not a large focus in the call-stack.
+
+ It is way easier to mock and control side-effects using this object.
+
+ - It does not have request extensions applied.
+ - Threadlocals are not properly pushed.
+
+ """
+ request = DummyRequest()
+ request.registry = app.registry
+ request.host = 'example.com'
+ request.dbsession = dbsession
+ request.tm = tm
+
+ return request
diff --git a/docs/tutorials/wiki2/src/models/tests/test_functional.py b/docs/tutorials/wiki2/src/models/tests/test_functional.py
new file mode 100644
index 000000000..dbcd8aec7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tests/test_functional.py
@@ -0,0 +1,13 @@
+from tutorial import models
+
+def test_my_view_success(testapp, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ res = testapp.get('/', status=200)
+ assert res.body
+
+def test_notfound(testapp):
+ res = testapp.get('/badurl', status=404)
+ assert res.status_code == 404
diff --git a/docs/tutorials/wiki2/src/models/tests/test_it.py b/docs/tutorials/wiki2/src/models/tests/test_it.py
deleted file mode 100644
index ea16534fc..000000000
--- a/docs/tutorials/wiki2/src/models/tests/test_it.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import unittest
-
-from pyramid import testing
-
-import transaction
-
-
-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('tutorial.models')
- settings = self.config.get_settings()
-
- from tutorial.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 tutorial.models.meta import Base
- Base.metadata.create_all(self.engine)
-
- def tearDown(self):
- from tutorial.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 tutorial.models import MyModel
-
- model = MyModel(name='one', value=55)
- self.session.add(model)
-
- def test_passing_view(self):
- from tutorial.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 tutorial.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/tests/test_views.py b/docs/tutorials/wiki2/src/models/tests/test_views.py
new file mode 100644
index 000000000..8ae464d03
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tests/test_views.py
@@ -0,0 +1,23 @@
+from tutorial import models
+from tutorial.views.default import my_view
+from tutorial.views.notfound import notfound_view
+
+
+def test_my_view_failure(app_request):
+ info = my_view(app_request)
+ assert info.status_int == 500
+
+def test_my_view_success(app_request, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ info = my_view(app_request)
+ assert app_request.response.status_int == 200
+ assert info['one'].name == 'one'
+ assert info['project'] == 'myproj'
+
+def test_notfound_view(app_request):
+ info = notfound_view(app_request)
+ assert app_request.response.status_int == 404
+ assert info == {}
diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
index 5c2ba5cc0..7edc0957d 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py
@@ -5,8 +5,8 @@ def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
with Configurator(settings=settings) as config:
- config.include('.models')
config.include('pyramid_jinja2')
config.include('.routes')
+ config.include('.models')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
index a4209a6e9..47d77ef01 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
@@ -66,13 +66,21 @@ def includeme(config):
# use pyramid_retry to retry a request when transient exceptions occur
config.include('pyramid_retry')
- session_factory = get_session_factory(get_engine(settings))
+ # hook to share the dbengine fixture in testing
+ dbengine = settings.get('dbengine')
+ if not dbengine:
+ dbengine = get_engine(settings)
+
+ session_factory = get_session_factory(dbengine)
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
- )
+ def dbsession(request):
+ # hook to share the dbsession fixture in testing
+ dbsession = request.environ.get('app.dbsession')
+ if dbsession is None:
+ # request.tm is the transaction manager used by pyramid_tm
+ dbsession = get_tm_session(session_factory, request.tm)
+ return dbsession
+
+ config.add_request_method(dbsession, reify=True)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py
index e6350fb36..c8034e5a5 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py
@@ -8,6 +8,10 @@ from .. import models
def setup_models(dbsession):
+ """
+ Add or update models / fixtures in the database.
+
+ """
editor = models.User(name='editor', role='editor')
editor.set_password('editor')
dbsession.add(editor)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
index 094b2f303..a0f654d38 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
@@ -1,7 +1,6 @@
from pyramid.view import view_config
from pyramid.response import Response
-
-from sqlalchemy.exc import DBAPIError
+from sqlalchemy.exc import SQLAlchemyError
from .. import models
@@ -10,8 +9,8 @@ from .. import models
def my_view(request):
try:
query = request.dbsession.query(models.MyModel)
- one = query.filter(models.MyModel.name == 'one').first()
- except DBAPIError:
+ one = query.filter(models.MyModel.name == 'one').one()
+ except SQLAlchemyError:
return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'myproj'}
--
cgit v1.2.3
From cd666082fbbd8b11d5cefd4a2d72209ae4f847be Mon Sep 17 00:00:00 2001
From: Michael Merickel
Date: Mon, 6 Jan 2020 22:58:07 -0600
Subject: sync views with new structure and add csrf protection
---
docs/tutorials/wiki2/definingviews.rst | 140 ++++++++++++---------
docs/tutorials/wiki2/src/views/.gitignore | 1 +
docs/tutorials/wiki2/src/views/setup.py | 2 +-
docs/tutorials/wiki2/src/views/testing.ini | 79 ++++++++++++
docs/tutorials/wiki2/src/views/tests/conftest.py | 125 ++++++++++++++++++
.../wiki2/src/views/tests/test_functional.py | 13 ++
docs/tutorials/wiki2/src/views/tests/test_it.py | 66 ----------
docs/tutorials/wiki2/src/views/tests/test_views.py | 23 ++++
.../tutorials/wiki2/src/views/tutorial/__init__.py | 3 +-
.../wiki2/src/views/tutorial/models/__init__.py | 22 ++--
.../src/views/tutorial/scripts/initialize_db.py | 4 +
.../tutorials/wiki2/src/views/tutorial/security.py | 6 +
.../wiki2/src/views/tutorial/templates/404.jinja2 | 6 +-
.../wiki2/src/views/tutorial/templates/edit.jinja2 | 3 +-
.../src/views/tutorial/templates/layout.jinja2 | 9 ++
.../wiki2/src/views/tutorial/views/default.py | 23 ++--
16 files changed, 375 insertions(+), 150 deletions(-)
create mode 100644 docs/tutorials/wiki2/src/views/testing.ini
create mode 100644 docs/tutorials/wiki2/src/views/tests/conftest.py
create mode 100644 docs/tutorials/wiki2/src/views/tests/test_functional.py
delete mode 100644 docs/tutorials/wiki2/src/views/tests/test_it.py
create mode 100644 docs/tutorials/wiki2/src/views/tests/test_views.py
create mode 100644 docs/tutorials/wiki2/src/views/tutorial/security.py
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index a434039ca..122164083 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -26,14 +26,15 @@ 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 the ``setup()`` function.
+list.
Open ``tutorial/setup.py`` and edit it to look like the following:
.. literalinclude:: src/views/setup.py
- :linenos:
- :emphasize-lines: 14
- :language: python
+ :lines: 11-31
+ :lineno-match:
+ :emphasize-lines: 4
+ :language: python
Only the highlighted line needs to be added.
@@ -50,7 +51,7 @@ were provided at the time we created the project.
As an example, the CSS file will be accessed via
``http://localhost:6543/static/theme.css`` by virtue of the call to the
-``add_static_view`` directive we've made in the ``routes.py`` file. Any number
+``add_static_view`` directive we've made in the ``tutorial/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(':static/foo.css')`` within
@@ -63,7 +64,7 @@ Adding routes to ``routes.py``
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.
-The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route`
+The ``tutorial/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.
@@ -96,13 +97,13 @@ order they're registered.
decorator attached to the ``edit_page`` view function, which in turn will be
indicated by ``route_name='edit_page'``.
-As a result of our edits, the ``routes.py`` file should look like the
+As a result of our edits, the ``tutorial/routes.py`` file should look like the
following:
.. literalinclude:: src/views/tutorial/routes.py
- :linenos:
- :emphasize-lines: 3-6
- :language: python
+ :linenos:
+ :emphasize-lines: 3-6
+ :language: python
The highlighted lines are the ones that need to be added or edited.
@@ -117,18 +118,41 @@ The highlighted lines are the ones that need to be added or edited.
behavior in your own apps.
+CSRF protection
+===============
+
+When handling HTML forms that mutate data in our database we need to verify that the form submission is legitimate and not from a URL embedded in a third-party website.
+This is done by adding a unique token to each form that a third-party could not easily guess.
+Read more about CSRF at :ref:`csrf_protection`.
+For this tutorial, we'll store the active CSRF token in a cookie.
+
+Let's add a new ``tutorial/security.py`` file:
+
+.. literalinclude:: src/views/tutorial/security.py
+ :linenos:
+ :emphasize-lines: 5-6
+ :language: python
+
+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/views/tutorial/__init__.py
+ :linenos:
+ :emphasize-lines: 9
+ :language: python
+
+On forms that mutate data, we'll be sure to add the CSRF token to the form, using :func:`pyramid.csrf.get_csrf_token`.
+
+
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:
+replace it with 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.
+ :linenos:
+ :language: python
We added some imports, and created a regular expression to find "WikiWords".
@@ -137,7 +161,7 @@ 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``
+Then we added four :term:`view callable` functions to our ``tutorial/views/default.py``
module, as mentioned in the previous step:
* ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL.
@@ -163,10 +187,10 @@ The ``view_wiki`` view function
Following is the code for the ``view_wiki`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views/default.py
- :lines: 17-20
- :lineno-match:
- :linenos:
- :language: python
+ :lines: 16-19
+ :lineno-match:
+ :linenos:
+ :language: python
``view_wiki()`` is the :term:`default view` that gets called when a request is
made to the root URL of our wiki. It always redirects to a URL which
@@ -174,12 +198,12 @@ 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
+:class:`pyramid.httpexceptions.HTTPSeeOther` class (instances of which implement
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.
+the "location" of the ``HTTPSeeOther`` response, forming an HTTP redirect.
The ``view_page`` view function
@@ -188,10 +212,10 @@ The ``view_page`` view function
Here is the code for the ``view_page`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views/default.py
- :lines: 22-42
- :lineno-match:
- :linenos:
- :language: python
+ :lines: 21-41
+ :lineno-match:
+ :linenos:
+ :language: python
``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
@@ -241,10 +265,10 @@ The ``edit_page`` view function
Here is the code for the ``edit_page`` view function and its decorator:
.. literalinclude:: src/views/tutorial/views/default.py
- :lines: 44-56
- :lineno-match:
- :linenos:
- :language: python
+ :lines: 43-55
+ :lineno-match:
+ :linenos:
+ :language: python
``edit_page()`` is invoked when a user clicks the "Edit this Page" button on
the view form. It renders an edit form, but it also acts as the handler for the
@@ -252,14 +276,13 @@ 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
+If the view execution *is* a result of a form submission (i.e., ``request.method == 'POST'``), 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
-expression ``'form.submitted' in request.params`` is ``False``), the view
+expression ``request.method != 'POST'``), 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.
@@ -279,10 +302,10 @@ 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
+ :lines: 57-
+ :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
@@ -301,7 +324,7 @@ 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
+``request.method == 'POST'``), 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
@@ -312,7 +335,7 @@ 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
+expression ``request.method != 'POST'`` 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
@@ -339,9 +362,9 @@ Update ``tutorial/templates/layout.jinja2`` with the following content, as
indicated by the emphasized lines:
.. literalinclude:: src/views/tutorial/templates/layout.jinja2
- :linenos:
- :emphasize-lines: 11,35-37
- :language: html
+ :linenos:
+ :emphasize-lines: 11,35-37
+ :language: html
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
@@ -350,8 +373,7 @@ template inheritance via blocks.
- We have defined two placeholders in the layout template where a child
template can override the content. These blocks are named ``subtitle`` (line
11) and ``content`` (line 36).
-- Please refer to the `Jinja2 documentation `_ for more information about template
- inheritance.
+- Please refer to the `Jinja2 documentation `_ for more information about template inheritance.
The ``view.jinja2`` template
@@ -360,8 +382,8 @@ The ``view.jinja2`` template
Create ``tutorial/templates/view.jinja2`` and add the following content:
.. literalinclude:: src/views/tutorial/templates/view.jinja2
- :linenos:
- :language: html
+ :linenos:
+ :language: html
This template is used by ``view_page()`` for displaying a single wiki page.
@@ -384,9 +406,9 @@ 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
+ :linenos:
+ :emphasize-lines: 1,3,12,13,15,18
+ :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
@@ -396,11 +418,13 @@ containing a form and which provides the following:
of the page (line 1).
- Override the ``subtitle`` block to affect the ```` tag in the
``head`` of the page (line 3).
+- Add the CSRF token to the form (line 13).
+ Without this line, attempts to edit the page would result in a ``400 Bad Request`` error.
- 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).
+ any existing page data when it is rendered (line 15).
+- A submit button (line 18).
- 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.
+ 12). The view will use the ``body`` value.
The ``404.jinja2`` template
@@ -409,16 +433,16 @@ The ``404.jinja2`` template
Replace ``tutorial/templates/404.jinja2`` with the following content:
.. literalinclude:: src/views/tutorial/templates/404.jinja2
- :linenos:
- :language: html
+ :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
+ :linenos:
+ :emphasize-lines: 6
+ :language: python
There are several important things to note about this configuration:
diff --git a/docs/tutorials/wiki2/src/views/.gitignore b/docs/tutorials/wiki2/src/views/.gitignore
index 1853d983c..c612e59f2 100644
--- a/docs/tutorials/wiki2/src/views/.gitignore
+++ b/docs/tutorials/wiki2/src/views/.gitignore
@@ -19,3 +19,4 @@ Data.fs*
.DS_Store
coverage
test
+*.sqlite
diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py
index 500c5e599..12eabaff2 100644
--- a/docs/tutorials/wiki2/src/views/setup.py
+++ b/docs/tutorials/wiki2/src/views/setup.py
@@ -20,8 +20,8 @@ requires = [
'pyramid_tm',
'SQLAlchemy',
'transaction',
- 'zope.sqlalchemy',
'waitress',
+ 'zope.sqlalchemy',
]
tests_require = [
diff --git a/docs/tutorials/wiki2/src/views/testing.ini b/docs/tutorials/wiki2/src/views/testing.ini
new file mode 100644
index 000000000..85e5e1ae9
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/testing.ini
@@ -0,0 +1,79 @@
+###
+# 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/testing.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, alembic
+
+[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.)
+
+[logger_alembic]
+level = WARN
+handlers =
+qualname = alembic
+
+[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/views/tests/conftest.py b/docs/tutorials/wiki2/src/views/tests/conftest.py
new file mode 100644
index 000000000..2db65f887
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tests/conftest.py
@@ -0,0 +1,125 @@
+import alembic
+import alembic.config
+import alembic.command
+import os
+from pyramid.paster import get_appsettings
+from pyramid.scripting import prepare
+from pyramid.testing import DummyRequest
+import pytest
+import transaction
+from webob.cookies import Cookie
+import webtest
+
+from tutorial import main
+from tutorial import models
+from tutorial.models.meta import Base
+
+
+def pytest_addoption(parser):
+ parser.addoption('--ini', action='store', metavar='INI_FILE')
+
+@pytest.fixture(scope='session')
+def ini_file(request):
+ # potentially grab this path from a pytest option
+ return os.path.abspath(request.config.option.ini or 'testing.ini')
+
+@pytest.fixture(scope='session')
+def app_settings(ini_file):
+ return get_appsettings(ini_file)
+
+@pytest.fixture(scope='session')
+def dbengine(app_settings, ini_file):
+ engine = models.get_engine(app_settings)
+
+ alembic_cfg = alembic.config.Config(ini_file)
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+ # run migrations to initialize the database
+ # depending on how we want to initialize the database from scratch
+ # we could alternatively call:
+ # Base.metadata.create_all(bind=engine)
+ # alembic.command.stamp(alembic_cfg, "head")
+ alembic.command.upgrade(alembic_cfg, "head")
+
+ yield engine
+
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+@pytest.fixture(scope='session')
+def app(app_settings, dbengine):
+ return main({}, dbengine=dbengine, **app_settings)
+
+@pytest.fixture
+def tm():
+ tm = transaction.TransactionManager(explicit=True)
+ tm.begin()
+ tm.doom()
+
+ yield tm
+
+ tm.abort()
+
+@pytest.fixture
+def dbsession(app, tm):
+ session_factory = app.registry['dbsession_factory']
+ return models.get_tm_session(session_factory, tm)
+
+@pytest.fixture
+def testapp(app, tm, dbsession):
+ # override request.dbsession and request.tm with our own
+ # externally-controlled values that are shared across requests but aborted
+ # at the end
+ testapp = webtest.TestApp(app, extra_environ={
+ 'HTTP_HOST': 'example.com',
+ 'tm.active': True,
+ 'tm.manager': tm,
+ 'app.dbsession': dbsession,
+ })
+
+ return testapp
+
+@pytest.fixture
+def app_request(app, tm, dbsession):
+ """
+ A real request.
+
+ This request is almost identical to a real request but it has some
+ drawbacks in tests as it's harder to mock data and is heavier.
+
+ """
+ env = prepare(registry=app.registry)
+ request = env['request']
+ request.host = 'example.com'
+
+ # without this, request.dbsession will be joined to the same transaction
+ # manager but it will be using a different sqlalchemy.orm.Session using
+ # a separate database transaction
+ request.dbsession = dbsession
+ request.tm = tm
+
+ yield request
+ env['closer']()
+
+@pytest.fixture
+def dummy_request(app, tm, dbsession):
+ """
+ A lightweight dummy request.
+
+ This request is ultra-lightweight and should be used only when the
+ request itself is not a large focus in the call-stack.
+
+ It is way easier to mock and control side-effects using this object.
+
+ - It does not have request extensions applied.
+ - Threadlocals are not properly pushed.
+
+ """
+ request = DummyRequest()
+ request.registry = app.registry
+ request.host = 'example.com'
+ request.dbsession = dbsession
+ request.tm = tm
+
+ return request
diff --git a/docs/tutorials/wiki2/src/views/tests/test_functional.py b/docs/tutorials/wiki2/src/views/tests/test_functional.py
new file mode 100644
index 000000000..dbcd8aec7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tests/test_functional.py
@@ -0,0 +1,13 @@
+from tutorial import models
+
+def test_my_view_success(testapp, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ res = testapp.get('/', status=200)
+ assert res.body
+
+def test_notfound(testapp):
+ res = testapp.get('/badurl', status=404)
+ assert res.status_code == 404
diff --git a/docs/tutorials/wiki2/src/views/tests/test_it.py b/docs/tutorials/wiki2/src/views/tests/test_it.py
deleted file mode 100644
index ea16534fc..000000000
--- a/docs/tutorials/wiki2/src/views/tests/test_it.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import unittest
-
-from pyramid import testing
-
-import transaction
-
-
-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('tutorial.models')
- settings = self.config.get_settings()
-
- from tutorial.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 tutorial.models.meta import Base
- Base.metadata.create_all(self.engine)
-
- def tearDown(self):
- from tutorial.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 tutorial.models import MyModel
-
- model = MyModel(name='one', value=55)
- self.session.add(model)
-
- def test_passing_view(self):
- from tutorial.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 tutorial.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/tests/test_views.py b/docs/tutorials/wiki2/src/views/tests/test_views.py
new file mode 100644
index 000000000..8ae464d03
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tests/test_views.py
@@ -0,0 +1,23 @@
+from tutorial import models
+from tutorial.views.default import my_view
+from tutorial.views.notfound import notfound_view
+
+
+def test_my_view_failure(app_request):
+ info = my_view(app_request)
+ assert info.status_int == 500
+
+def test_my_view_success(app_request, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ info = my_view(app_request)
+ assert app_request.response.status_int == 200
+ assert info['one'].name == 'one'
+ assert info['project'] == 'myproj'
+
+def test_notfound_view(app_request):
+ info = notfound_view(app_request)
+ assert app_request.response.status_int == 404
+ assert info == {}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
index 5c2ba5cc0..81a22c68c 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py
@@ -5,8 +5,9 @@ def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
with Configurator(settings=settings) as config:
- config.include('.models')
config.include('pyramid_jinja2')
+ config.include('.security')
config.include('.routes')
+ config.include('.models')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
index a4209a6e9..47d77ef01 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
@@ -66,13 +66,21 @@ def includeme(config):
# use pyramid_retry to retry a request when transient exceptions occur
config.include('pyramid_retry')
- session_factory = get_session_factory(get_engine(settings))
+ # hook to share the dbengine fixture in testing
+ dbengine = settings.get('dbengine')
+ if not dbengine:
+ dbengine = get_engine(settings)
+
+ session_factory = get_session_factory(dbengine)
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
- )
+ def dbsession(request):
+ # hook to share the dbsession fixture in testing
+ dbsession = request.environ.get('app.dbsession')
+ if dbsession is None:
+ # request.tm is the transaction manager used by pyramid_tm
+ dbsession = get_tm_session(session_factory, request.tm)
+ return dbsession
+
+ config.add_request_method(dbsession, reify=True)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py
index e6350fb36..c8034e5a5 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py
@@ -8,6 +8,10 @@ from .. import models
def setup_models(dbsession):
+ """
+ Add or update models / fixtures in the database.
+
+ """
editor = models.User(name='editor', role='editor')
editor.set_password('editor')
dbsession.add(editor)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/security.py b/docs/tutorials/wiki2/src/views/tutorial/security.py
new file mode 100644
index 000000000..216894e07
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/security.py
@@ -0,0 +1,6 @@
+from pyramid.csrf import CookieCSRFStoragePolicy
+
+
+def includeme(config):
+ config.set_csrf_storage_policy(CookieCSRFStoragePolicy())
+ config.set_default_csrf_options(require_csrf=True)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2
index aaf12413f..5edb15285 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2
+++ b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2
@@ -1,8 +1,6 @@
{% extends "layout.jinja2" %}
{% block content %}
-
diff --git a/docs/tutorials/wiki/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki/src/tests/tutorial/views/auth.py
new file mode 100644
index 000000000..5062779a6
--- /dev/null
+++ b/docs/tutorials/wiki/src/tests/tutorial/views/auth.py
@@ -0,0 +1,51 @@
+from pyramid.httpexceptions import HTTPSeeOther
+from pyramid.security import (
+ forget,
+ remember,
+)
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..security import check_password, USERS
+
+
+@view_config(context='..models.Wiki', name='login',
+ renderer='tutorial:templates/login.pt')
+@forbidden_view_config(renderer='tutorial:templates/login.pt')
+def login(request):
+ login_url = request.resource_url(request.root, '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 check_password(USERS.get(login), password):
+ headers = remember(request, login)
+ return HTTPSeeOther(location=came_from, headers=headers)
+ message = 'Failed login'
+ request.response.status = 400
+
+ return dict(
+ message=message,
+ url=login_url,
+ came_from=came_from,
+ login=login,
+ password=password,
+ title='Login',
+ )
+
+
+@view_config(context='..models.Wiki', name='logout')
+def logout(request):
+ headers = forget(request)
+ return HTTPSeeOther(
+ location=request.resource_url(request.context),
+ headers=headers,
+ )
diff --git a/docs/tutorials/wiki/src/tests/tutorial/views/default.py b/docs/tutorials/wiki/src/tests/tutorial/views/default.py
index 7ba99c65b..5bb21fbcd 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/views/default.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/views/default.py
@@ -1,30 +1,21 @@
from docutils.core import publish_parts
+from pyramid.httpexceptions import HTTPSeeOther
+from pyramid.view import view_config
import re
-from pyramid.httpexceptions import HTTPFound
-from pyramid.security import (
- forget,
- remember,
-)
-from pyramid.view import (
- forbidden_view_config,
- view_config,
- )
-
from ..models import Page
-from ..security import check_password, USERS
+
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-
-@view_config(context='..models.Wiki',
- permission='view')
+@view_config(context='..models.Wiki', permission='view')
def view_wiki(context, request):
- return HTTPFound(location=request.resource_url(context, 'FrontPage'))
+ return HTTPSeeOther(location=request.resource_url(context, 'FrontPage'))
-@view_config(context='..models.Page', renderer='tutorial:templates/view.pt',
+@view_config(context='..models.Page',
+ renderer='tutorial:templates/view.pt',
permission='view')
def view_page(context, request):
wiki = context.__parent__
@@ -42,8 +33,7 @@ def view_page(context, request):
page_text = publish_parts(context.data, writer_name='html')['html_body']
page_text = wikiwords.sub(check, page_text)
edit_url = request.resource_url(context, 'edit_page')
- return dict(page=context, page_text=page_text, edit_url=edit_url,
- logged_in=request.authenticated_userid)
+ return dict(page=context, page_text=page_text, edit_url=edit_url)
@view_config(name='add_page', context='..models.Wiki',
@@ -57,13 +47,12 @@ def add_page(context, request):
page.__name__ = pagename
page.__parent__ = context
context[pagename] = page
- return HTTPFound(location=request.resource_url(page))
+ return HTTPSeeOther(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)
+ return dict(page=page, save_url=save_url)
@view_config(name='edit_page', context='..models.Page',
@@ -72,46 +61,9 @@ 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 dict(page=context,
- save_url=request.resource_url(context, 'edit_page'),
- logged_in=request.authenticated_userid)
-
-
-@view_config(context='..models.Wiki', name='login',
- renderer='tutorial:templates/login.pt')
-@forbidden_view_config(renderer='tutorial:templates/login.pt')
-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
- 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 check_password(USERS.get(login), password):
- headers = remember(request, login)
- return HTTPFound(location=came_from,
- headers=headers)
- message = 'Failed login'
+ return HTTPSeeOther(location=request.resource_url(context))
return dict(
- message=message,
- url=request.application_url + '/login',
- came_from=came_from,
- login=login,
- password=password,
- title='Login',
+ page=context,
+ save_url=request.resource_url(context, 'edit_page'),
)
-
-
-@view_config(context='..models.Wiki', name='logout')
-def logout(request):
- headers = forget(request)
- return HTTPFound(location=request.resource_url(request.context),
- headers=headers)
diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst
index f710b3b10..e563b174e 100644
--- a/docs/tutorials/wiki/tests.rst
+++ b/docs/tutorials/wiki/tests.rst
@@ -4,45 +4,98 @@
Adding Tests
============
-We will now add tests for the models and the views and a few functional tests in ``tests/test_it.py``.
+We will now add tests for the models and the views and a few functional tests in the ``tests`` package.
Tests ensure that an application works, and that it continues to work when changes are made in the future.
-Test the models
-===============
+Test harness
+============
-We write tests for the ``model`` classes and the ``appmaker``.
-We will modify our ``test_it.py`` file, writing a separate test class for each ``model`` class.
-We will also write a test class for the ``appmaker``.
+The project came bootstrapped with some tests and a basic harness.
+These are located in the ``tests`` package at the top-level of the project.
+It is a common practice to put tests into a ``tests`` package alongside the application package, especially as projects grow in size and complexity.
+A useful convention is for each module in the application to contain a corresponding module in the ``tests`` package.
+The test module would have the same name with the prefix ``test_``.
-We will add three test classes, one for each of the following:
+The harness consists of the following setup:
-- the ``Page`` model named ``PageModelTests``
-- the ``Wiki`` model named ``WikiModelTests``
-- the appmaker named ``AppmakerTests``
+- ``pytest.ini`` - controls basic ``pytest`` config including where to find the tests.
+ We have configured ``pytest`` to search for tests in the application package and in the ``tests`` package.
+- ``.coveragerc`` - controls coverage config.
+ In our setup, it works with the ``pytest-cov`` plugin that we use via the ``--cov`` options to the ``pytest`` command.
-Test the views
-==============
+- ``testing.ini`` - a mirror of ``development.ini`` and ``production.ini`` that contains settings used for executing the test suite.
+ Most importantly, it contains the database connection information used by tests that require the database.
-We will modify our ``test_it.py`` file, adding tests for each view function that we added previously.
-As a result, we will 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.
+- ``tests_require`` in ``setup.py`` - controls the dependencies installed when testing.
+ When the list is changed, it's necessary to re-run ``$VENV/bin/pip install -e ".[testing]"`` to ensure the new dependencies are installed.
+- ``tests/conftest.py`` - the core fixtures available throughout our tests.
+ The fixtures are explained in more detail below.
-Functional tests
-================
-We will test the whole application, covering security aspects that are not tested in the unit tests, such as logging in, logging out, checking that the ``viewer`` user cannot add or edit pages, but the ``editor`` user can, and so on.
-As a result we will add two test classes, ``SecurityTests`` and ``FunctionalTests``.
+Session-scoped test fixtures
+----------------------------
+
+- ``app_settings`` - the settings ``dict`` parsed from the ``testing.ini`` file that would normally be passed by ``pserve`` into your app's ``main`` function.
+
+- ``app`` - the :app:`Pyramid` WSGI application, implementing the :class:`pyramid.interfaces.IRouter` interface.
+ Most commonly this would be used for functional tests.
+
+
+Per-test fixtures
+-----------------
+
+- ``tm`` - a :class:`transaction.TransactionManager` object controlling a transaction lifecycle.
+ Generally other fixtures would join to the ``tm`` fixture to control their lifecycle and ensure they are aborted at the end of the test.
+
+- ``testapp`` - a :class:`webtest.TestApp` instance wrapping the ``app`` and is used to sending requests into the application and return full response objects that can be inspected.
+ The ``testapp`` is able to mutate the request environ such that the ``tm`` fixture is injected and used by any code that's touching ``request.tm``.
+ This should join the ``request.root`` ZODB model to the transaction manager as well, to enable rolling back changes to the database.
+ The ``testapp`` maintains a cookiejar, so it can be used to share state across requests, as well as the transaction database connection.
+
+- ``app_request`` - a :class:`pyramid.request.Request` object that can be used for more lightweight tests versus the full ``testapp``.
+ The ``app_request`` can be passed to view functions and other code that need a fully functional request object.
+
+- ``dummy_request`` - a :class:`pyramid.testing.DummyRequest` object that is very lightweight.
+ This is a great object to pass to view functions that have minimal side-effects as it'll be fast and simple.
+
+
+Unit tests
+==========
+
+We can test individual APIs within our codebase to ensure they fulfill the expected contract that the rest of the application expects.
+For example, we'll test the password hashing features we added to ``tutorial.security`` and the rest of our models.
+
+Create ``tests/test_models.py`` such that it appears as follows:
+.. literalinclude:: src/tests/tests/test_models.py
+ :linenos:
+ :language: python
+
+
+Integration tests
+=================
+
+We can directly execute the view code, bypassing :app:`Pyramid` and testing just the code that we've written.
+These tests use dummy requests that we'll prepare appropriately to set the conditions each view expects.
+
+Update ``tests/test_views.py`` such that it appears as follows:
+
+.. literalinclude:: src/tests/tests/test_views.py
+ :linenos:
+ :language: python
+
+
+Functional tests
+================
-View the results of all our edits to ``tests/test_it.py``
-=========================================================
+We'll test the whole application, covering security aspects that are not tested in the unit and integration 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.
-Open the ``tests/test_it.py`` module, and edit it such that it appears as follows:
+Update ``tests/test_functional.py`` such that it appears as follows:
-.. literalinclude:: src/tests/tests/test_it.py
+.. literalinclude:: src/tests/tests/test_functional.py
:linenos:
:language: python
@@ -72,4 +125,4 @@ The expected result should look like the following:
.. code-block:: text
.........................
- 25 passed in 6.87 seconds
+ 25 passed in 3.87 seconds
--
cgit v1.2.3
From b4eda298728883eaa767b1fd21146c6b4202eb17 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 00:26:32 -0800
Subject: grammar fix
---
docs/tutorials/wiki2/index.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst
index 40a194155..69e69684b 100644
--- a/docs/tutorials/wiki2/index.rst
+++ b/docs/tutorials/wiki2/index.rst
@@ -8,7 +8,7 @@ This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based
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 `GitHub `_ for a specific branch or version under ``docs/tutorials/wiki2/src``,
+tutorial can be browsed on `GitHub `_ for a specific branch or version under ``docs/tutorials/wiki2/src``,
which corresponds to the same location if you have Pyramid sources.
.. toctree::
--
cgit v1.2.3
From 3a23085dc7ad597900c0ad4cbca8a342329386d2 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 01:01:42 -0800
Subject: Update version numbers since last run to the latest
---
docs/tutorials/wiki2/installation.rst | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 9defef31a..3da8a8f17 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -179,15 +179,15 @@ The console will show ``pip`` checking for packages and installing missing packa
Successfully installed Jinja2-2.10.3 Mako-1.1.0 MarkupSafe-1.1.1 \
PasteDeploy-2.0.1 Pygments-2.5.2 SQLAlchemy-1.3.12 WebTest-2.0.33 \
- alembic-1.3.2 attrs-19.3.0 beautifulsoup4-4.8.2 coverage-5.0.1 \
- hupper-1.9.1 importlib-metadata-1.3.0 more-itertools-8.0.2 packaging-19.2 \
+ alembic-1.3.2 attrs-19.3.0 beautifulsoup4-4.8.2 coverage-5.0.3 \
+ hupper-1.9.1 importlib-metadata-1.4.0 more-itertools-8.1.0 packaging-20.0 \
plaster-1.0 plaster-pastedeploy-0.7 pluggy-0.13.1 py-1.8.1 \
pyparsing-2.4.6 pyramid-1.10.4 pyramid-debugtoolbar-4.5.2 \
pyramid-jinja2-2.8 pyramid-mako-1.1.0 pyramid-retry-2.1 pyramid-tm-2.4 \
pytest-5.3.2 pytest-cov-2.8.1 python-dateutil-2.8.1 python-editor-1.0.4 \
repoze.lru-0.7 six-1.13.0 soupsieve-1.9.5 transaction-3.0.0 \
- translationstring-1.3 tutorial venusian-3.0.0 waitress-1.4.1 \
- wcwidth-0.1.7 webob-1.8.5 zipp-0.6.0 zope.deprecation-4.4.0 \
+ translationstring-1.3 tutorial venusian-3.0.0 waitress-1.4.2 \
+ wcwidth-0.1.8 webob-1.8.5 zipp-0.6.0 zope.deprecation-4.4.0 \
zope.interface-4.7.1 zope.sqlalchemy-1.2
Testing requirements are defined in our project's ``setup.py`` file, in the ``tests_require`` and ``extras_require`` stanzas.
--
cgit v1.2.3
From 715988f6946402a940a4b670b2a1b9ae7423b0aa Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 01:19:22 -0800
Subject: Improve requires section - Improve narrative flow - Remove
test_requires from literalinclude
---
docs/tutorials/wiki2/definingmodels.rst | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst
index f84ca6588..129d77806 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -29,10 +29,10 @@ We need to add a dependency, the `bcrypt `_ pa
package's ``setup.py`` file by assigning this dependency to the ``requires``
parameter in the ``setup()`` function.
-Open ``tutorial/setup.py`` and edit it to look like the following:
+Open ``tutorial/setup.py`` and edit it to look like the following by adding ``bcrypt`` and sorting the packages:
.. literalinclude:: src/models/setup.py
- :lines: 11-30
+ :lines: 11-24
:linenos:
:lineno-match:
:emphasize-lines: 3
@@ -40,7 +40,6 @@ Open ``tutorial/setup.py`` and edit it to look like the following:
It is a good practice to sort packages alphabetically to make them easier to find.
Our cookiecutter does not have its packages sorted because it merely tacks on additional packages depending on our selections.
-After adding ``bcrypt`` and sorting packages, we should have the above ``requires`` list.
.. note::
--
cgit v1.2.3
From fd553ef2592587422bfce1139cfba3d8945882c1 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 01:29:43 -0800
Subject: Remove tests_require from literalinclude
---
docs/tutorials/wiki2/definingviews.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index 122164083..b3c9487ec 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -31,7 +31,7 @@ list.
Open ``tutorial/setup.py`` and edit it to look like the following:
.. literalinclude:: src/views/setup.py
- :lines: 11-31
+ :lines: 11-25
:lineno-match:
:emphasize-lines: 4
:language: python
--
cgit v1.2.3
From a3b0a6d480ad0995d3acd4ae6d7c731bbe8cbea5 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 01:35:53 -0800
Subject: Remove emphasize-lines because the entire module is new, and there is
no narrative reference to the emphasized lines
---
docs/tutorials/wiki2/definingviews.rst | 1 -
1 file changed, 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index b3c9487ec..ca2760de5 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -130,7 +130,6 @@ Let's add a new ``tutorial/security.py`` file:
.. literalinclude:: src/views/tutorial/security.py
:linenos:
- :emphasize-lines: 5-6
:language: python
Since we've added a new ``tutorial/security.py`` module, we need to include it.
--
cgit v1.2.3
From ef8bf6f1c6b616812a96e7b582bcdf2f7038e7f3 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 01:41:02 -0800
Subject: Remove emphasize-lines because the entire module is new and
highlighting doesn't help here, just adds maintenance
---
docs/tutorials/wiki2/definingviews.rst | 1 -
1 file changed, 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index ca2760de5..367b45163 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -406,7 +406,6 @@ Create ``tutorial/templates/edit.jinja2`` and add the following content:
.. literalinclude:: src/views/tutorial/templates/edit.jinja2
:linenos:
- :emphasize-lines: 1,3,12,13,15,18
:language: html
This template serves two use cases. It is used by ``add_page()`` and
--
cgit v1.2.3
From 24c9a330c0941e0e63be4e0809c72d972a2fb7fa Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 01:43:46 -0800
Subject: Align emphasis to method name
---
docs/tutorials/wiki2/definingviews.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index 367b45163..c4712faf0 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -439,7 +439,7 @@ This template is linked from the ``notfound_view`` defined in
.. literalinclude:: src/views/tutorial/views/notfound.py
:linenos:
- :emphasize-lines: 6
+ :emphasize-lines: 5
:language: python
There are several important things to note about this configuration:
--
cgit v1.2.3
From 05f9ff6e2d1d89af68a70ab52894f6575377f78a Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 01:49:57 -0800
Subject: Align line numbers to code - @mmerickel can you check the wording to
ensure it is still correct for the code?
---
docs/tutorials/wiki2/authentication.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
index c799d79bf..5e0077b20 100644
--- a/docs/tutorials/wiki2/authentication.rst
+++ b/docs/tutorials/wiki2/authentication.rst
@@ -43,8 +43,8 @@ Update ``tutorial/security.py`` with the following content:
Here we've defined:
-* A new security policy named ``MySecurityPolicy``, which is implementing most of the :class:`pyramid.interfaces.ISecurityPolicy` interface by tracking a :term:`identity` using a signed cookie implemented by :class:`pyramid.authentication.AuthTktCookieHelper` (lines 7-29).
-* The ``request.user`` computed property is registered for use throughout our application as the authenticated ``tutorial.models.User`` object for the logged-in user (line 38-39).
+* A new security policy named ``MySecurityPolicy``, which is implementing most of the :class:`pyramid.interfaces.ISecurityPolicy` interface by tracking a :term:`identity` using a signed cookie implemented by :class:`pyramid.authentication.AuthTktCookieHelper` (lines 8-34).
+* The ``request.user`` computed property is registered for use throughout our application as the authenticated ``tutorial.models.User`` object for the logged-in user (line 42-44).
Our new :term:`security policy` defines how our application will remember, forget, and identify users.
It also handles authorization, which we'll cover in the next chapter (if you're wondering why we didn't implement the ``permits`` method yet).
--
cgit v1.2.3
From 459b0c7051f5a0c4e4ef7adf1e51e3548dba6b39 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 01:51:37 -0800
Subject: Use reST numbered list syntax, not markdown
---
docs/tutorials/wiki2/authentication.rst | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
index 5e0077b20..25240b191 100644
--- a/docs/tutorials/wiki2/authentication.rst
+++ b/docs/tutorials/wiki2/authentication.rst
@@ -49,20 +49,20 @@ Here we've defined:
Our new :term:`security policy` defines how our application will remember, forget, and identify users.
It also handles authorization, which we'll cover in the next chapter (if you're wondering why we didn't implement the ``permits`` method yet).
-Identifying the current user is done in a couple steps:
+Identifying the current user is done in a few steps:
-1. :app:`Pyramid` invokes a method on the policy requesting identity, userid, or permission to perform an operation.
+#. :app:`Pyramid` invokes a method on the policy requesting identity, userid, or permission to perform an operation.
-1. The policy starts by calling :meth:`pyramid.request.RequestLocalCache.get_or_create` to load the identity.
+#. The policy starts by calling :meth:`pyramid.request.RequestLocalCache.get_or_create` to load the identity.
-1. The ``MySecurityPolicy.load_identity`` method asks the cookie helper to pull the identity from the request.
+#. The ``MySecurityPolicy.load_identity`` method asks the cookie helper to pull the identity from the request.
This value is ``None`` if the cookie is missing or the content cannot be verified.
-1. The policy then translates the identity into a ``tutorial.models.User`` object by looking for a record in the database.
+#. The policy then translates the identity into a ``tutorial.models.User`` object by looking for a record in the database.
This is a good spot to confirm that the user is actually allowed to access our application.
For example, maybe they were marked deleted or banned and we should return ``None`` instead of the ``user`` object.
-1. The result is stored in the ``identity_cache`` which ensures that subsequent invocations return the same identity object for the request.
+#. The result is stored in the ``identity_cache`` which ensures that subsequent invocations return the same identity object for the request.
Finally, :attr:`pyramid.request.Request.authenticated_identity` contains either ``None`` or a ``tutorial.models.User`` instance and that value is aliased to ``request.user`` for convenience in our application.
--
cgit v1.2.3
From 6ca6370c751dd7a629f7057a79941764c71d4aeb Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 02:07:07 -0800
Subject: Use correct verb
---
docs/tutorials/wiki2/authentication.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
index 25240b191..5519a967e 100644
--- a/docs/tutorials/wiki2/authentication.rst
+++ b/docs/tutorials/wiki2/authentication.rst
@@ -129,7 +129,7 @@ Open the file ``tutorial/views/default.py`` and fix the following import:
:emphasize-lines: 2
:language: python
-Change the highlighted line.
+Insert the highlighted line.
In the same file, now edit the ``edit_page`` view function:
--
cgit v1.2.3
From 57e7a4a3571bb6ea1bfd767c2c50674ede944ca6 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 02:13:25 -0800
Subject: from webob.cookies import Cookie already existed; restore deleted
comment
---
docs/tutorials/wiki2/src/tests/tests/conftest.py | 1 +
docs/tutorials/wiki2/tests.rst | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/src/tests/tests/conftest.py b/docs/tutorials/wiki2/src/tests/tests/conftest.py
index 094bc06f1..1c8fb16d0 100644
--- a/docs/tutorials/wiki2/src/tests/tests/conftest.py
+++ b/docs/tutorials/wiki2/src/tests/tests/conftest.py
@@ -39,6 +39,7 @@ def dbengine(app_settings, ini_file):
# depending on how we want to initialize the database from scratch
# we could alternatively call:
# Base.metadata.create_all(bind=engine)
+ # alembic.command.stamp(alembic_cfg, "head")
alembic.command.upgrade(alembic_cfg, "head")
yield engine
diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst
index 8a3e79363..cea07eeca 100644
--- a/docs/tutorials/wiki2/tests.rst
+++ b/docs/tutorials/wiki2/tests.rst
@@ -88,7 +88,7 @@ Update ``tests/conftest.py`` to look like the following, adding the highlighted
.. literalinclude:: src/tests/tests/conftest.py
:linenos:
- :emphasize-lines: 10,68-103,110,117-119
+ :emphasize-lines: 68-103,110,117-119
:language: python
--
cgit v1.2.3
From 84225a4db96dc08fa5923c29777fadbbaad0a098 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 02:14:38 -0800
Subject: realign emphasize-lines
---
docs/tutorials/wiki2/tests.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst
index cea07eeca..03e90458c 100644
--- a/docs/tutorials/wiki2/tests.rst
+++ b/docs/tutorials/wiki2/tests.rst
@@ -88,7 +88,7 @@ Update ``tests/conftest.py`` to look like the following, adding the highlighted
.. literalinclude:: src/tests/tests/conftest.py
:linenos:
- :emphasize-lines: 68-103,110,117-119
+ :emphasize-lines: 69-104,111,118-120
:language: python
--
cgit v1.2.3
From 56fd7cbeaf107405cca2399d2b0bba60c8e60010 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 05:19:17 -0800
Subject: grammar fix
---
docs/tutorials/wiki/index.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki/index.rst b/docs/tutorials/wiki/index.rst
index 7bd58656b..a45e7f3e2 100644
--- a/docs/tutorials/wiki/index.rst
+++ b/docs/tutorials/wiki/index.rst
@@ -10,7 +10,7 @@ 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 `GitHub `_ for a specific branch or version under ``docs/tutorials/wiki/src``,
+tutorial can be browsed on `GitHub `_ for a specific branch or version under ``docs/tutorials/wiki/src``,
which corresponds to the same location if you have Pyramid sources.
.. toctree::
--
cgit v1.2.3
From 04cef9070f8554a9a025ae968368d03b4ed939c2 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Mon, 13 Jan 2020 05:32:13 -0800
Subject: update output of package versions, coverage. fix command.
---
docs/tutorials/wiki/installation.rst | 32 ++++++++++++++++----------------
1 file changed, 16 insertions(+), 16 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst
index 392441eae..ae8b2b8f6 100644
--- a/docs/tutorials/wiki/installation.rst
+++ b/docs/tutorials/wiki/installation.rst
@@ -188,15 +188,15 @@ The console will show ``pip`` checking for packages and installing missing packa
Successfully installed BTrees-4.6.1 Chameleon-3.6.2 Mako-1.1.0 \
MarkupSafe-1.1.1 PasteDeploy-2.0.1 Pygments-2.5.2 WebTest-2.0.33 \
ZConfig-3.5.0 ZEO-5.2.1 ZODB-5.5.1 ZODB3-3.11.0 attrs-19.3.0 \
- beautifulsoup4-4.8.2 cffi-1.13.2 coverage-5.0.1 hupper-1.9.1 \
- importlib-metadata-1.3.0 more-itertools-8.0.2 packaging-19.2 \
+ beautifulsoup4-4.8.2 cffi-1.13.2 coverage-5.0.3 hupper-1.9.1 \
+ importlib-metadata-1.4.0 more-itertools-8.1.0 packaging-20.0 \
persistent-4.5.1 plaster-1.0 plaster-pastedeploy-0.7 pluggy-0.13.1 \
py-1.8.1 pycparser-2.19 pyparsing-2.4.6 pyramid-1.10.4 \
- pyramid-chameleon-0.3 pyramid-debugtoolbar-4.5.1 pyramid-mako-1.1.0 \
- pyramid-retry-2.1 pyramid-tm-2.3 pyramid-zodbconn-0.8.1 pytest-5.3.2 \
+ pyramid-chameleon-0.3 pyramid-debugtoolbar-4.5.2 pyramid-mako-1.1.0 \
+ pyramid-retry-2.1 pyramid-tm-2.4 pyramid-zodbconn-0.8.1 pytest-5.3.2 \
pytest-cov-2.8.1 repoze.lru-0.7 six-1.13.0 soupsieve-1.9.5 \
transaction-3.0.0 translationstring-1.3 tutorial venusian-3.0.0 \
- waitress-1.4.1 wcwidth-0.1.7 webob-1.8.5 zc.lockfile-2.0 zdaemon-4.3 \
+ waitress-1.4.2 wcwidth-0.1.8 webob-1.8.5 zc.lockfile-2.0 zdaemon-4.3 \
zipp-0.6.0 zodbpickle-2.0.0 zodburi-2.4.0 zope.deprecation-4.4.0 \
zope.interface-4.7.1
@@ -279,25 +279,25 @@ If successful, you will see output something like this:
platform darwin -- Python 3.7.3, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
rootdir: /filepath/tutorial, inifile: pytest.ini, testpaths: tutorial
plugins: cov-2.8.1
- collected 2 items
+ collected 4 items
- tutorial/tests.py .. [100%]
+ tests/test_functional.py .. [ 50%]
+ tests/test_views.py .. [100%]
-
- ---------- coverage: platform darwin, python 3.7.0-final-0 -----------
+ ---------- coverage: platform darwin, python 3.7.3-final-0 -----------
Name Stmts Miss Cover Missing
-----------------------------------------------------------
- tutorial/__init__.py 16 11 31% 7-8, 14-22
- tutorial/models/__init__.py 8 4 50% 9-12
- tutorial/pshell.py 6 6 0% 1-12
- tutorial/routes.py 2 2 0% 1-2
+ tutorial/__init__.py 16 0 100%
+ tutorial/models/__init__.py 8 0 100%
+ tutorial/pshell.py 6 4 33% 5-12
+ tutorial/routes.py 2 0 100%
tutorial/views/__init__.py 0 0 100%
tutorial/views/default.py 4 0 100%
tutorial/views/notfound.py 4 0 100%
-----------------------------------------------------------
- TOTAL 40 23 42%
+ TOTAL 40 4 90%
- ===================== 2 passed in 0.55 seconds =======================
+ ===================== 4 passed in 0.85 seconds =======================
Our package doesn't quite have 100% test coverage.
@@ -317,7 +317,7 @@ On Unix
.. code-block:: bash
- $VENV/bin/pytest --cov=tutorial tests.py -q
+ $VENV/bin/pytest --cov=tutorial tests -q
On Windows
^^^^^^^^^^
--
cgit v1.2.3
From 0be9d736de379efcdc858936cb91f7edb2fa5563 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Tue, 14 Jan 2020 03:34:04 -0800
Subject: Update test output
---
docs/tutorials/wiki2/tests.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst
index 03e90458c..9c2312ec5 100644
--- a/docs/tutorials/wiki2/tests.rst
+++ b/docs/tutorials/wiki2/tests.rst
@@ -150,7 +150,7 @@ The expected result should look like the following:
.. code-block:: text
- ...............................
- 31 passed in 8.85 seconds
+ ........................... [100%]
+ 27 passed in 6.91s
.. _webtest: https://docs.pylonsproject.org/projects/webtest/en/latest/
--
cgit v1.2.3
From 6d3e713a603b2b3663bb77af9884505419f8879c Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Tue, 14 Jan 2020 03:39:49 -0800
Subject: Revert emphasis removal
---
docs/tutorials/wiki2/tests.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst
index 9c2312ec5..1bf38d988 100644
--- a/docs/tutorials/wiki2/tests.rst
+++ b/docs/tutorials/wiki2/tests.rst
@@ -88,7 +88,7 @@ Update ``tests/conftest.py`` to look like the following, adding the highlighted
.. literalinclude:: src/tests/tests/conftest.py
:linenos:
- :emphasize-lines: 69-104,111,118-120
+ :emphasize-lines: 10,69-104,111,118-120
:language: python
--
cgit v1.2.3
From 907c7c0ff9732ecdafab2d6b0c599d86fe0e6a80 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Tue, 14 Jan 2020 03:47:31 -0800
Subject: Remove Windows command prompt
---
docs/tutorials/wiki2/installation.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 3da8a8f17..8c897fc86 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -342,7 +342,7 @@ On Windows
.. code-block:: doscon
- c:\tutorial> %VENV%\Scripts\pytest --cov --cov-report=term-missing
+ %VENV%\Scripts\pytest --cov --cov-report=term-missing
If successful, you will see output something like this:
--
cgit v1.2.3
From 87f6d25d27bb3b08e620f7aed206687a93fedde5 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Tue, 14 Jan 2020 03:59:22 -0800
Subject: Align Windows command with Unix
---
docs/tutorials/wiki2/installation.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 8c897fc86..1e22fb624 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -403,7 +403,7 @@ On Windows
.. code-block:: doscon
- %VENV%\Scripts\pytest --cov=tutorial tutorial\tests.py -q
+ %VENV%\Scripts\pytest --cov=tutorial tests -q
pytest follows :ref:`conventions for Python test discovery
`, and the configuration defaults from the cookiecutter
--
cgit v1.2.3
From 3152aa5e4b1cf053f94725d3dc8069bfe20a14be Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Wed, 15 Jan 2020 03:29:58 -0800
Subject: Remove bits that demo what one would need to do without defaults -
Sync up wiki with language from wiki2
---
docs/tutorials/wiki/installation.rst | 17 -----------------
docs/tutorials/wiki2/installation.rst | 29 +++++------------------------
2 files changed, 5 insertions(+), 41 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst
index ae8b2b8f6..4de9b4b9c 100644
--- a/docs/tutorials/wiki/installation.rst
+++ b/docs/tutorials/wiki/installation.rst
@@ -309,23 +309,6 @@ 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
-^^^^^^^
-
-.. code-block:: bash
-
- $VENV/bin/pytest --cov=tutorial tests -q
-
-On Windows
-^^^^^^^^^^
-
-.. code-block:: doscon
-
- %VENV%\Scripts\pytest --cov=tutorial tests -q
-
``pytest`` follows :ref:`conventions for Python test discovery `.
The configuration defaults from the cookiecutter tell ``pytest`` where to find the module on which we want to run tests and coverage.
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 1e22fb624..f016f19df 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -386,32 +386,13 @@ Our package doesn't quite have 100% test coverage.
Test and coverage cookiecutter defaults
---------------------------------------
-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.
+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.
-On Unix
-^^^^^^^
-
-.. code-block:: bash
-
- $VENV/bin/pytest --cov=tutorial tests -q
-
-On Windows
-^^^^^^^^^^
-
-.. code-block:: doscon
-
- %VENV%\Scripts\pytest --cov=tutorial tests -q
-
-pytest follows :ref:`conventions for Python 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.
+``pytest`` follows :ref:`conventions for Python test discovery `.
+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.
+.. seealso:: See ``pytest``'s documentation for :ref:`pytest:usage` or invoke ``pytest -h`` to see its full set of options.
.. _wiki2-start-the-application:
--
cgit v1.2.3
From f2c900097e709c71c893cce6492c6d8d1cdb25f2 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Wed, 15 Jan 2020 05:25:13 -0800
Subject: Update emphasize-lines for definingmodels.rst to clarify changes and
fix alignment.
---
docs/tutorials/wiki/definingmodels.rst | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst
index d4402915a..3b1e38c7d 100644
--- a/docs/tutorials/wiki/definingmodels.rst
+++ b/docs/tutorials/wiki/definingmodels.rst
@@ -43,8 +43,11 @@ Open ``tutorial/models/__init__.py`` file and edit it to look like the following
.. literalinclude:: src/models/tutorial/models/__init__.py
:linenos:
+ :emphasize-lines: 1,5-11,15-19
:language: python
+The emphasized lines indicate changes, described as follows.
+
Remove the ``MyModel`` class from the generated ``models/__init__.py`` file.
The ``MyModel`` class is only a sample and we're not going to use it.
@@ -62,6 +65,7 @@ Then we add a ``Wiki`` class.
.. literalinclude:: src/models/tutorial/models/__init__.py
:pyobject: Wiki
:lineno-match:
+ :emphasize-lines: 1-3
:language: py
We want it to inherit from the :class:`persistent.mapping.PersistentMapping` class because it provides mapping behavior.
@@ -76,6 +80,7 @@ Now we add a ``Page`` class.
.. literalinclude:: src/models/tutorial/models/__init__.py
:pyobject: Page
:lineno-match:
+ :emphasize-lines: 1-3
:language: py
This class should inherit from the :class:`persistent.Persistent` class.
@@ -93,7 +98,7 @@ As a last step, edit the ``appmaker`` function.
.. literalinclude:: src/models/tutorial/models/__init__.py
:pyobject: appmaker
:lineno-match:
- :emphasize-lines: 4-8
+ :emphasize-lines: 3-7
:language: py
The :term:`root` :term:`resource` of our application is a Wiki instance.
--
cgit v1.2.3
From 8322a2e409c94000761b9fc69fd5914c6cab9bb7 Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Wed, 15 Jan 2020 05:51:25 -0800
Subject: Update output of install of docutils - Fix line number reference
---
docs/tutorials/wiki/definingviews.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst
index 5aafd68d6..02d7bde9a 100644
--- a/docs/tutorials/wiki/definingviews.rst
+++ b/docs/tutorials/wiki/definingviews.rst
@@ -74,7 +74,7 @@ Success executing this command will end with a line to the console similar to th
.. code-block:: text
- Successfully installed docutils-0.15.2 tutorial
+ Successfully installed docutils-0.16 tutorial
Adding view functions in the ``views`` package
@@ -291,7 +291,7 @@ We can do this via :term:`METAL` macros and slots.
- The cookiecutter defined a macro named ``layout`` (line 1).
This macro consists of the entire template.
-- We changed the ``title`` tag to use the ``name`` attribute of a ``page`` object, or if it does not exist then the page title (lines 11-12).
+- We changed the ``title`` tag to use the ``name`` attribute of a ``page`` object, or if it does not exist then the page title (line 11).
- The cookiecutter defined a macro customization point or `slot` (line 35).
This slot is inside the macro ``layout``.
Therefore it can be replaced by content, customizing the macro.
--
cgit v1.2.3
From c963dd0b6aefa148a486d58f0621e83f53ea95cb Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Wed, 15 Jan 2020 23:21:18 -0800
Subject: Minor grammar fixes - Swap order of editing tutorial/views/default.py
so that line numbers in the user's editor align with the rendered docs
---
docs/tutorials/wiki/authorization.rst | 42 +++++++++++++++++------------------
1 file changed, 20 insertions(+), 22 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index 1469fae44..995dfa729 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -78,7 +78,7 @@ Open the file ``tutorial/__init__.py`` and edit the following lines:
The security policy controls several aspects of authentication and authorization:
-- Identifying the current user / :term:`identity` for a ``request``.
+- Identifying the current user's :term:`identity` for a ``request``.
- Authorizating access to resources.
@@ -90,7 +90,7 @@ Identifying logged-in users
The ``MySecurityPolicy.authenticated_identity`` method inspects the ``request`` and determines if it came from an authenticated user.
It does this by utilizing the :class:`pyramid.authentication.AuthTktCookieHelper` class which stores the :term:`identity` in a cryptographically-signed cookie.
-If a ``request`` does contain an identity then we perform a final check to determine if the user is valid in our current ``USERS`` store.
+If a ``request`` does contain an identity, then we perform a final check to determine if the user is valid in our current ``USERS`` store.
Authorizing access to resources
@@ -111,9 +111,9 @@ For our application we've defined a list of a few principals:
- :attr:`pyramid.security.Authenticated`
- :attr:`pyramid.security.Everyone`
-Later, various wiki pages will grant some of these principals access to edit, or add new pages.
+Various wiki pages will grant some of these principals access to edit existing or add new pages.
-Finally, there are two helper methods that will help us later to authenticate users.
+Finally there are two helper methods that will help us 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
@@ -140,8 +140,8 @@ the file ``development.ini`` and add the highlighted line below:
:lineno-match:
:language: ini
-Finally, best practices tell us to use a different secret in each environment, so
-open ``production.ini`` and add a different secret:
+Best practices tell us to use a different secret in each environment.
+Open ``production.ini`` and add a different secret:
.. literalinclude:: src/authorization/production.ini
:lines: 17-19
@@ -149,7 +149,7 @@ open ``production.ini`` and add a different secret:
:lineno-match:
:language: ini
-And ``testing.ini``:
+Edit ``testing.ini`` to add its unique secret:
.. literalinclude:: src/authorization/testing.ini
:lines: 17-19
@@ -202,44 +202,42 @@ We actually need only *one* ACL for the entire system, however, because our secu
Add permission declarations
~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/views/default.py`` and add a ``permission='edit'`` parameter to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
+Open ``tutorial/views/default.py``.
+Add a ``permission='view'`` parameter to the ``@view_config`` decorators for ``view_wiki()`` and ``view_page()`` as follows:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 39-41
+ :lines: 12
:lineno-match:
- :emphasize-lines: 2-3
+ :emphasize-lines: 1
:language: python
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 58-60
+ :lines: 17-19
:lineno-match:
:emphasize-lines: 2-3
:language: python
Only the highlighted lines, along with their preceding commas, need to be edited and added.
-The result is that only users who possess the ``edit`` permission at the time of the request may invoke those two views.
+This allows anyone to invoke these two views.
-Add a ``permission='view'`` parameter to the ``@view_config`` decorator for
-``view_wiki()`` as follows:
+Next add a ``permission='edit'`` parameter to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 12
+ :lines: 39-41
:lineno-match:
- :emphasize-lines: 1
+ :emphasize-lines: 2-3
:language: python
-And ``view_page()`` as follows:
-
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 17-19
+ :lines: 58-60
:lineno-match:
:emphasize-lines: 2-3
:language: python
Only the highlighted lines, along with their preceding commas, need to be edited and added.
-This allows anyone to invoke these two views.
+The result is that only users who possess the ``edit`` permission at the time of the request may invoke those two views.
We are done with the changes needed to control access.
The changes that follow will add the login and logout feature.
@@ -290,8 +288,8 @@ Create ``tutorial/templates/login.pt`` with the following content:
The above template is referenced in the login view that we just added in ``views.py``.
-Add a "Login" and "Logout" links
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Add "Login" and "Logout" links
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Open ``tutorial/templates/layout.pt`` and add the following code as indicated by the highlighted lines.
--
cgit v1.2.3
From cc26acfd29c94036d1c4d9164dba6a2b7792c00a Mon Sep 17 00:00:00 2001
From: Steve Piercy
Date: Wed, 15 Jan 2020 23:36:43 -0800
Subject: Minor grammar fixes - Expand contractions and spell out words
---
docs/tutorials/wiki/tests.rst | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
(limited to 'docs/tutorials')
diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst
index e563b174e..231945c9a 100644
--- a/docs/tutorials/wiki/tests.rst
+++ b/docs/tutorials/wiki/tests.rst
@@ -19,7 +19,7 @@ The test module would have the same name with the prefix ``test_``.
The harness consists of the following setup:
-- ``pytest.ini`` - controls basic ``pytest`` config including where to find the tests.
+- ``pytest.ini`` - controls basic ``pytest`` configuration, including where to find the tests.
We have configured ``pytest`` to search for tests in the application package and in the ``tests`` package.
- ``.coveragerc`` - controls coverage config.
@@ -29,10 +29,11 @@ The harness consists of the following setup:
Most importantly, it contains the database connection information used by tests that require the database.
- ``tests_require`` in ``setup.py`` - controls the dependencies installed when testing.
- When the list is changed, it's necessary to re-run ``$VENV/bin/pip install -e ".[testing]"`` to ensure the new dependencies are installed.
+ When the list is changed, it is necessary to re-run ``$VENV/bin/pip install -e ".[testing]"`` to ensure the new dependencies are installed.
- ``tests/conftest.py`` - the core fixtures available throughout our tests.
- The fixtures are explained in more detail below.
+ The fixtures are explained in more detail in the following sections.
+ Open ``tests/conftest.py`` and follow along.
Session-scoped test fixtures
@@ -51,7 +52,7 @@ Per-test fixtures
Generally other fixtures would join to the ``tm`` fixture to control their lifecycle and ensure they are aborted at the end of the test.
- ``testapp`` - a :class:`webtest.TestApp` instance wrapping the ``app`` and is used to sending requests into the application and return full response objects that can be inspected.
- The ``testapp`` is able to mutate the request environ such that the ``tm`` fixture is injected and used by any code that's touching ``request.tm``.
+ The ``testapp`` is able to mutate the request environ such that the ``tm`` fixture is injected and used by any code that touches ``request.tm``.
This should join the ``request.root`` ZODB model to the transaction manager as well, to enable rolling back changes to the database.
The ``testapp`` maintains a cookiejar, so it can be used to share state across requests, as well as the transaction database connection.
@@ -59,14 +60,14 @@ Per-test fixtures
The ``app_request`` can be passed to view functions and other code that need a fully functional request object.
- ``dummy_request`` - a :class:`pyramid.testing.DummyRequest` object that is very lightweight.
- This is a great object to pass to view functions that have minimal side-effects as it'll be fast and simple.
+ This is a great object to pass to view functions that have minimal side-effects as it will be fast and simple.
Unit tests
==========
We can test individual APIs within our codebase to ensure they fulfill the expected contract that the rest of the application expects.
-For example, we'll test the password hashing features we added to ``tutorial.security`` and the rest of our models.
+For example, we will test the password hashing features we added to ``tutorial.security`` and the rest of our models.
Create ``tests/test_models.py`` such that it appears as follows:
@@ -78,8 +79,8 @@ Create ``tests/test_models.py`` such that it appears as follows:
Integration tests
=================
-We can directly execute the view code, bypassing :app:`Pyramid` and testing just the code that we've written.
-These tests use dummy requests that we'll prepare appropriately to set the conditions each view expects.
+We can directly execute the view code, bypassing :app:`Pyramid` and testing just the code that we have written.
+These tests use dummy requests that we will prepare appropriately to set the conditions each view expects.
Update ``tests/test_views.py`` such that it appears as follows:
@@ -91,7 +92,7 @@ Update ``tests/test_views.py`` such that it appears as follows:
Functional tests
================
-We'll test the whole application, covering security aspects that are not tested in the unit and integration 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.
+We will test the whole application, covering security aspects that are not tested in the unit and integration tests, like logging in, logging out, checking that the ``basic`` user cannot edit pages that it did not create, but that the ``editor`` user can, and so on.
Update ``tests/test_functional.py`` such that it appears as follows:
--
cgit v1.2.3