From 35dc507d9394a7dc0834838a5a596f4e47ab95fb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 4 Feb 2016 23:38:43 -0600 Subject: update source for basiclayout --- docs/tutorials/wiki2/src/basiclayout/MANIFEST.in | 2 +- .../tutorials/wiki2/src/basiclayout/production.ini | 2 - .../wiki2/src/basiclayout/tutorial/__init__.py | 2 +- .../src/basiclayout/tutorial/models/__init__.py | 71 +++++++++++++++++++++- .../wiki2/src/basiclayout/tutorial/models/meta.py | 33 ---------- .../src/basiclayout/tutorial/models/mymodel.py | 3 +- .../basiclayout/tutorial/scripts/initializedb.py | 16 ++--- .../src/basiclayout/tutorial/templates/404.jinja2 | 8 +++ .../wiki2/src/basiclayout/tutorial/tests.py | 18 +++--- .../src/basiclayout/tutorial/views/default.py | 4 +- .../wiki2/src/basiclayout/tutorial/views/errors.py | 5 ++ 11 files changed, 104 insertions(+), 60 deletions(-) create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in +++ b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py index 7994bbfa8..17763812a 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py @@ -6,7 +6,7 @@ def main(global_config, **settings): """ config = Configurator(settings=settings) config.include('pyramid_jinja2') - config.include('.models.meta') + config.include('.models') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py index 6ffc10a78..a4026fcd6 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import MyModel # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py index 5a2b5890c..d65a01a42 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py @@ -1,4 +1,3 @@ -from .meta import Base from sqlalchemy import ( Column, Index, @@ -6,6 +5,8 @@ from sqlalchemy import ( Text, ) +from .meta import Base + class MyModel(Base): __tablename__ = 'models' diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py index f0d09729e..da63c180a 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py @@ -9,13 +9,13 @@ from pyramid.paster import ( from pyramid.scripts.common import parse_vars -from ..models.meta import ( +from ..models import ( Base, - get_session, get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import MyModel +from ..models import MyModel def usage(argv): @@ -34,12 +34,12 @@ def main(argv=sys.argv): settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + model = MyModel(name='one', value=1) dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py index b947e3bb1..c54945c28 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py @@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( - get_session, + from .models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py index 13ad8793c..ad0c728d7 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py @@ -3,7 +3,7 @@ from pyramid.view import view_config from sqlalchemy.exc import DBAPIError -from ..models.mymodel import MyModel +from ..models import MyModel @view_config(route_name='home', renderer='../templates/mytemplate.jinja2') @@ -12,7 +12,7 @@ def my_view(request): query = request.dbsession.query(MyModel) one = query.filter(MyModel.name == 'one').first() except DBAPIError: - return Response(db_err_msg, content_type='text/plain', status_int=500) + return Response(db_err_msg, content_type='text/plain', status=500) return {'one': one, 'project': 'tutorial'} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 9b83981f85c1d822ecf5a9aa2a4fde1851c0d0cd Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 4 Feb 2016 23:50:00 -0600 Subject: fix the Base import --- docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py index da63c180a..7307ecc5c 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py @@ -9,8 +9,8 @@ from pyramid.paster import ( from pyramid.scripts.common import parse_vars +from ..models.meta import Base from ..models import ( - Base, get_engine, get_session_factory, get_tm_session, -- cgit v1.2.3 From 8ec0b01ef0de3d7859e081652ef752d9af612fc5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 5 Feb 2016 00:04:23 -0600 Subject: unindent literalincludes --- docs/tutorials/wiki2/basiclayout.rst | 166 +++++++++++++++++------------------ 1 file changed, 83 insertions(+), 83 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index e3d0a0a3c..d55ce807f 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -19,25 +19,25 @@ code. Open ``tutorial/tutorial/__init__.py``. It should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :linenos: + :language: py Let's go over this piece-by-piece. First, we need some imports to support later code: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :end-before: main - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :end-before: main + :linenos: + :language: py ``__init__.py`` defines a function named ``main``. Here is the entirety of the ``main`` function we've defined in our ``__init__.py``: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :pyobject: main - :lineno-start: 4 - :linenos: +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :pyobject: main + :lineno-start: 4 + :linenos: :language: py When you invoke the ``pserve development.ini`` command, the ``main`` function @@ -46,10 +46,10 @@ application. (See :ref:`startup_chapter` for more about ``pserve``.) Next in ``main``, construct a :term:`Configurator` object: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 7 - :lineno-start: 7 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 7 + :lineno-start: 7 + :language: py ``settings`` is passed to the Configurator as a keyword argument with the dictionary values passed as the ``**settings`` argument. This will be a @@ -60,26 +60,26 @@ deployment-related values such as ``pyramid.reload_templates``, 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-start: 8 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 8 + :lineno-start: 8 + :language: py Next include the module ``meta`` from the package ``models`` using a dotted Python path. - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 9 - :lineno-start: 9 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 9 + :lineno-start: 9 + :language: py ``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with two arguments: ``static`` (the name), and ``static`` (the path): - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 10 - :lineno-start: 10 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 10 + :lineno-start: 10 + :language: py This registers a static resource view which will match any URL that starts with the prefix ``/static`` (by virtue of the first argument to @@ -95,10 +95,10 @@ Using the configurator ``main`` also registers a :term:`route configuration` via the :meth:`pyramid.config.Configurator.add_route` method that will be used when the URL is ``/``: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 11 - :lineno-start: 11 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 11 + :lineno-start: 11 + :language: py Since this route has a ``pattern`` equaling ``/``, it is the route that will be matched when the URL ``/`` is visited, e.g., ``http://localhost:6543/``. @@ -110,19 +110,19 @@ other special) decorators. When it finds a ``@view_config`` decorator, a view configuration will be registered, which will allow one of our application URLs to be mapped to some code. - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 12 - :lineno-start: 12 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 12 + :lineno-start: 12 + :language: py Finally ``main`` is finished configuring things, so it uses the :meth:`pyramid.config.Configurator.make_wsgi_app` method to return a :term:`WSGI` application: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 - :lineno-start: 13 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 13 + :lineno-start: 13 + :language: py View declarations via the ``views`` package @@ -136,9 +136,9 @@ corresponding :term:`route`. Our application uses the Open ``tutorial/tutorial/views/default.py`` in the ``views`` package. It should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/views/default.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/views/default.py + :linenos: + :language: py The important part here is that the ``@view_config`` decorator associates the function it decorates (``my_view``) with a :term:`view configuration`, @@ -181,9 +181,9 @@ scaffold put the classes that implement our models. First, open ``tutorial/tutorial/models/__init__.py``, which should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/models/__init__.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :linenos: + :language: py Our ``__init__.py`` will perform some imports to support later code, then calls the function :func:`sqlalchemy.orm.configure_mappers`. @@ -191,17 +191,17 @@ the function :func:`sqlalchemy.orm.configure_mappers`. Next open ``tutorial/tutorial/models/meta.py``, which should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :linenos: + :language: py ``meta.py`` contains imports that are used to support later code. We create a dictionary ``NAMING_CONVENTION`` as well. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :end-before: metadata - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :end-before: metadata + :linenos: + :language: py Next we create a ``metadata`` object from the class :class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value @@ -210,60 +210,60 @@ for the ``naming_convention`` argument. We also need to create a declarative will inherit from the ``Base`` class so they can be associated with our particular database connection. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :lines: 18-19 - :lineno-start: 18 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :lines: 18-19 + :lineno-start: 18 + :linenos: + :language: py Next we define several functions, the first of which is ``includeme``, which configures various database settings by calling subsequently defined functions. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: includeme - :lineno-start: 22 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: includeme + :lineno-start: 22 + :linenos: + :language: py The function ``get_session`` registers a database session with a transaction manager, and returns a ``dbsession`` object. With the transaction manager, our application will automatically issue a transaction commit after every request unless an exception is raised, in which case the transaction will be aborted. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_session - :lineno-start: 35 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_session + :lineno-start: 35 + :linenos: + :language: py The ``get_engine`` function creates an :term:`SQLAlchemy` database engine using :func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.``-prefixed settings in the ``development.ini`` file's ``[app:main]`` section, which is a URI, something like ``sqlite://``. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_engine - :lineno-start: 42 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_engine + :lineno-start: 42 + :linenos: + :language: py The function ``get_dbmaker`` accepts an :term:`SQLAlchemy` database engine, and creates a database session object ``dbmaker`` from the :term:`SQLAlchemy` class :class:`sqlalchemy.orm.session.sessionmaker`, which is then used for creating a session with the database engine. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_dbmaker - :lineno-start: 46 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_dbmaker + :lineno-start: 46 + :linenos: + :language: py To give a simple example of a model class, we define one named ``MyModel``: - .. literalinclude:: src/basiclayout/tutorial/models/mymodel.py - :pyobject: MyModel - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py + :pyobject: MyModel + :linenos: + :language: py Our example model does not require an ``__init__`` method because SQLAlchemy supplies for us a default constructor if one is not already present, which @@ -282,5 +282,5 @@ class. That's about all there is to it regarding models, views, and initialization code in our stock application. -The Index import and the Index object creation is not required for this -tutorial, and will be removed in the next step. +The ``Index`` import and the ``Index`` object creation is not required for +this tutorial, and will be removed in the next step. -- cgit v1.2.3 From 7b89a7e435b9edb4da6976e9185ae425717d4085 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 5 Feb 2016 01:00:49 -0600 Subject: update basiclayout prose --- docs/tutorials/wiki2/basiclayout.rst | 168 ++++++++++++++++++++--------------- 1 file changed, 98 insertions(+), 70 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index d55ce807f..976f12e90 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -29,6 +29,7 @@ later code: .. literalinclude:: src/basiclayout/tutorial/__init__.py :end-before: main :linenos: + :lineno-match: :language: py ``__init__.py`` defines a function named ``main``. Here is the entirety of @@ -36,9 +37,9 @@ the ``main`` function we've defined in our ``__init__.py``: .. literalinclude:: src/basiclayout/tutorial/__init__.py :pyobject: main - :lineno-start: 4 :linenos: - :language: py + :lineno-match: + :language: py When you invoke the ``pserve development.ini`` command, the ``main`` function above is executed. It accepts some settings and returns a :term:`WSGI` @@ -48,7 +49,7 @@ Next in ``main``, construct a :term:`Configurator` object: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 7 - :lineno-start: 7 + :lineno-match: :language: py ``settings`` is passed to the Configurator as a keyword argument with the @@ -62,15 +63,15 @@ with the ``.jinja2`` extension within our project. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 8 - :lineno-start: 8 + :lineno-match: :language: py -Next include the module ``meta`` from the package ``models`` using a dotted -Python path. +Next include the the package ``models`` using a dotted Python path. The +exact setup of the models will be covered later. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 9 - :lineno-start: 9 + :lineno-match: :language: py ``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with @@ -78,7 +79,7 @@ two arguments: ``static`` (the name), and ``static`` (the path): .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 10 - :lineno-start: 10 + :lineno-match: :language: py This registers a static resource view which will match any URL that starts @@ -97,7 +98,7 @@ used when the URL is ``/``: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 11 - :lineno-start: 11 + :lineno-match: :language: py Since this route has a ``pattern`` equaling ``/``, it is the route that will @@ -112,7 +113,7 @@ application URLs to be mapped to some code. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 12 - :lineno-start: 12 + :lineno-match: :language: py Finally ``main`` is finished configuring things, so it uses the @@ -178,25 +179,16 @@ In a SQLAlchemy-based application, a *model* object is an object composed by querying the SQL database. The ``models`` package is where the ``alchemy`` scaffold put the classes that implement our models. -First, open ``tutorial/tutorial/models/__init__.py``, which should already -contain the following: - -.. literalinclude:: src/basiclayout/tutorial/models/__init__.py - :linenos: - :language: py - -Our ``__init__.py`` will perform some imports to support later code, then calls -the function :func:`sqlalchemy.orm.configure_mappers`. - -Next open ``tutorial/tutorial/models/meta.py``, which should already contain +First, open ``tutorial/tutorial/models/meta.py``, which should already contain the following: .. literalinclude:: src/basiclayout/tutorial/models/meta.py :linenos: :language: py -``meta.py`` contains imports that are used to support later code. We create a -dictionary ``NAMING_CONVENTION`` as well. +``meta.py`` contains imports and support code for defining the models. We +create a dictionary ``NAMING_CONVENTION`` as well for consistent naming of +support objects like indices and constraints. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :end-before: metadata @@ -205,82 +197,118 @@ dictionary ``NAMING_CONVENTION`` as well. Next we create a ``metadata`` object from the class :class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value -for the ``naming_convention`` argument. We also need to create a declarative -``Base`` object to use as a base class for our model. Then our model classes -will inherit from the ``Base`` class so they can be associated with our -particular database connection. +for the ``naming_convention`` argument. + +A ``MetaData`` object represents the table and other schema definitions for +a single database. We also need to create a declarative ``Base`` object to use +as a base class for our models. Our models will inherit from this ``Base`` +which will attach the tables to the ``metadata`` we created and define our +application's database schema. .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :lines: 18-19 - :lineno-start: 18 + :lines: 15-16 + :lineno-match: :linenos: :language: py -Next we define several functions, the first of which is ``includeme``, which -configures various database settings by calling subsequently defined functions. +We've defined the ``models`` as a packge to make it straightforward to +define models separately in different modules. To give a simple example of a +model class, we define one named ``MyModel`` in a ``mymodel.py``: -.. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: includeme - :lineno-start: 22 +.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py + :pyobject: MyModel :linenos: :language: py -The function ``get_session`` registers a database session with a transaction -manager, and returns a ``dbsession`` object. With the transaction manager, our -application will automatically issue a transaction commit after every request -unless an exception is raised, in which case the transaction will be aborted. +Our example model does not require an ``__init__`` method because SQLAlchemy +supplies for us a default constructor if one is not already present, which +accepts keyword arguments of the same name as that of the mapped attributes. -.. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_session - :lineno-start: 35 +.. note:: Example usage of MyModel: + + .. code-block:: python + + johnny = MyModel(name="John Doe", value=10) + +The ``MyModel`` class has a ``__tablename__`` attribute. This informs +SQLAlchemy which table to use to store the data representing instances of this +class. + +Finally, open ``tutorial/tutorial/models/__init__.py``, which should already +contain the following: + +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py :linenos: :language: py -The ``get_engine`` function creates an :term:`SQLAlchemy` database engine using -:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.``-prefixed -settings in the ``development.ini`` file's ``[app:main]`` section, which is a -URI, something like ``sqlite://``. +Our ``models/__init__.py`` module defines the primary API we will use for +configuring the database connections within our application and it contains +several functions we will cover below. + +As we mentioned above, the purpose of the ``models.meta.metadata`` object is +to describe the schema of the database and this is done by defining models +that inherit from the ``Base`` attached to that ``metadata`` object. In +Python, code is only executed if it is imported and so to attach the +``models`` table, defined in ``mymodel.py`` to the ``metadata`` we must +import it. If we skip this step then later when we run ``metadata.create_all`` +the table will not be created because the ``metadata`` does not know about it! +Another important reason to import all of the models is that when +defining relationships between models they must all exist in order for +SQLAlchemy to find and build those internal mappings. This is why after +importing all the models we explicitly execute the function +:func:`sqlalchemy.orm.configure_mappers`, once we are sure all the models have +been defined and before we start creating connections. + +Next we define several functions for connecting to our database. The first +and lowest level is the ``get_engine`` function which creates an +:term:`SQLAlchemy` database engine using :func:`sqlalchemy.engine_from_config` +from the ``sqlalchemy.``-prefixed settings in the ``development.ini`` +file's ``[app:main]`` section, which is a URI (something like ``sqlite://``). -.. literalinclude:: src/basiclayout/tutorial/models/meta.py +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py :pyobject: get_engine - :lineno-start: 42 + :lineno-match: :linenos: :language: py -The function ``get_dbmaker`` accepts an :term:`SQLAlchemy` database engine, -and creates a database session object ``dbmaker`` from the :term:`SQLAlchemy` +The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database +engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` class :class:`sqlalchemy.orm.session.sessionmaker`, which is then used for -creating a session with the database engine. +creating sessions bound to the database engine. -.. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_dbmaker - :lineno-start: 46 +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: get_session_factory + :lineno-match: :linenos: :language: py -To give a simple example of a model class, we define one named ``MyModel``: +The function ``get_tm_session`` registers a database session with a transaction +manager, and returns a ``dbsession`` object. With the transaction manager, our +application will automatically issue a transaction commit after every request +unless an exception is raised, in which case the transaction will be aborted. -.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py - :pyobject: MyModel +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: get_tm_session + :lineno-match: :linenos: :language: py -Our example model does not require an ``__init__`` method because SQLAlchemy -supplies for us a default constructor if one is not already present, which -accepts keyword arguments of the same name as that of the mapped attributes. - -.. note:: Example usage of MyModel: - - .. code-block:: python - - johnny = MyModel(name="John Doe", value=10) +Finally, we define an ``includeme`` function, which is a hook for use with +:meth:`pyramid.config.Configurator.include` to activate code in a Pyramid +application addon. It is the code that is executed above when we ran +``config.include('.models')`` in our application's ``main`` function. This +function will take the settings from the application, create an engine +and define a ``request.dbsession`` property which we can use to do work +on behalf of an incoming request to our application. -The ``MyModel`` class has a ``__tablename__`` attribute. This informs -SQLAlchemy which table to use to store the data representing instances of this -class. +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: includeme + :lineno-match: + :linenos: + :language: py That's about all there is to it regarding models, views, and initialization code in our stock application. -The ``Index`` import and the ``Index`` object creation is not required for -this tutorial, and will be removed in the next step. +The ``Index`` import and the ``Index`` object creation in ``mymodel.py`` is +not required for this tutorial, and will be removed in the next step. -- cgit v1.2.3 From 21d69fd97b66401264746bb7dad1e8d4bd2491b9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 5 Feb 2016 01:09:11 -0600 Subject: link to create_all sqla method --- docs/tutorials/wiki2/basiclayout.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 976f12e90..6d1ff73a0 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -250,8 +250,10 @@ to describe the schema of the database and this is done by defining models that inherit from the ``Base`` attached to that ``metadata`` object. In Python, code is only executed if it is imported and so to attach the ``models`` table, defined in ``mymodel.py`` to the ``metadata`` we must -import it. If we skip this step then later when we run ``metadata.create_all`` -the table will not be created because the ``metadata`` does not know about it! +import it. If we skip this step then later when we run +:meth:`sqlalchemy.schema.MetaData.create_all` the table will not be created +because the ``metadata`` does not know about it! + Another important reason to import all of the models is that when defining relationships between models they must all exist in order for SQLAlchemy to find and build those internal mappings. This is why after -- cgit v1.2.3 From ab68f1f89a0a1602078bf1a99741d0635ce06dd0 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 5 Feb 2016 01:28:18 -0800 Subject: minor grammar and punctuation tweaks, break up run-on sentences. --- docs/tutorials/wiki2/basiclayout.rst | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 6d1ff73a0..3533bb455 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -201,8 +201,8 @@ for the ``naming_convention`` argument. A ``MetaData`` object represents the table and other schema definitions for a single database. We also need to create a declarative ``Base`` object to use -as a base class for our models. Our models will inherit from this ``Base`` -which will attach the tables to the ``metadata`` we created and define our +as a base class for our models. Our models will inherit from this ``Base``, +which will attach the tables to the ``metadata`` we created, and define our application's database schema. .. literalinclude:: src/basiclayout/tutorial/models/meta.py @@ -242,30 +242,30 @@ contain the following: :language: py Our ``models/__init__.py`` module defines the primary API we will use for -configuring the database connections within our application and it contains +configuring the database connections within our application, and it contains several functions we will cover below. As we mentioned above, the purpose of the ``models.meta.metadata`` object is -to describe the schema of the database and this is done by defining models -that inherit from the ``Base`` attached to that ``metadata`` object. In -Python, code is only executed if it is imported and so to attach the -``models`` table, defined in ``mymodel.py`` to the ``metadata`` we must -import it. If we skip this step then later when we run -:meth:`sqlalchemy.schema.MetaData.create_all` the table will not be created +to describe the schema of the database. This is done by defining models that +inherit from the ``Base`` attached to that ``metadata`` object. In Python, code +is only executed if it is imported, and so to attach the ``models`` table +defined in ``mymodel.py`` to the ``metadata``, we must import it. If we skip +this step, then later, when we run +:meth:`sqlalchemy.schema.MetaData.create_all`, the table will not be created because the ``metadata`` does not know about it! -Another important reason to import all of the models is that when -defining relationships between models they must all exist in order for -SQLAlchemy to find and build those internal mappings. This is why after -importing all the models we explicitly execute the function +Another important reason to import all of the models is that, when defining +relationships between models, they must all exist in order for SQLAlchemy to +find and build those internal mappings. This is why, after importing all the +models, we explicitly execute the function :func:`sqlalchemy.orm.configure_mappers`, once we are sure all the models have been defined and before we start creating connections. -Next we define several functions for connecting to our database. The first -and lowest level is the ``get_engine`` function which creates an -:term:`SQLAlchemy` database engine using :func:`sqlalchemy.engine_from_config` -from the ``sqlalchemy.``-prefixed settings in the ``development.ini`` -file's ``[app:main]`` section, which is a URI (something like ``sqlite://``). +Next we define several functions for connecting to our database. The first and +lowest level is the ``get_engine`` function. This creates an :term:`SQLAlchemy` +database engine using :func:`sqlalchemy.engine_from_config` from the +``sqlalchemy.``-prefixed settings in the ``development.ini`` file's +``[app:main]`` section. This setting is a URI (something like ``sqlite://``). .. literalinclude:: src/basiclayout/tutorial/models/__init__.py :pyobject: get_engine @@ -274,9 +274,9 @@ file's ``[app:main]`` section, which is a URI (something like ``sqlite://``). :language: py The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database -engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` -class :class:`sqlalchemy.orm.session.sessionmaker`, which is then used for -creating sessions bound to the database engine. +engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` class +:class:`sqlalchemy.orm.session.sessionmaker`. This ``session_factory`` is then +used for creating sessions bound to the database engine. .. literalinclude:: src/basiclayout/tutorial/models/__init__.py :pyobject: get_session_factory @@ -286,7 +286,7 @@ creating sessions bound to the database engine. The function ``get_tm_session`` registers a database session with a transaction manager, and returns a ``dbsession`` object. With the transaction manager, our -application will automatically issue a transaction commit after every request +application will automatically issue a transaction commit after every request, unless an exception is raised, in which case the transaction will be aborted. .. literalinclude:: src/basiclayout/tutorial/models/__init__.py @@ -297,10 +297,10 @@ unless an exception is raised, in which case the transaction will be aborted. Finally, we define an ``includeme`` function, which is a hook for use with :meth:`pyramid.config.Configurator.include` to activate code in a Pyramid -application addon. It is the code that is executed above when we ran +application add-on. It is the code that is executed above when we ran ``config.include('.models')`` in our application's ``main`` function. This -function will take the settings from the application, create an engine -and define a ``request.dbsession`` property which we can use to do work +function will take the settings from the application, create an engine, +and define a ``request.dbsession`` property, which we can use to do work on behalf of an incoming request to our application. .. literalinclude:: src/basiclayout/tutorial/models/__init__.py -- cgit v1.2.3 From d1cb34643e086ac74965455b486ce0058764324f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 13:57:51 -0600 Subject: assume the user is in the tutorial folder this is already assumed inside of installation where commands are run relative to setup.py --- docs/tutorials/wiki2/basiclayout.rst | 8 ++++---- docs/tutorials/wiki2/definingmodels.rst | 13 +++++++------ docs/tutorials/wiki2/definingviews.rst | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 6d1ff73a0..eb315d2cb 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -16,7 +16,7 @@ package. We use ``__init__.py`` both as a marker, indicating the directory in which it's contained is a package, and to contain application configuration code. -Open ``tutorial/tutorial/__init__.py``. It should already contain the +Open ``tutorial/__init__.py``. It should already contain the following: .. literalinclude:: src/basiclayout/tutorial/__init__.py @@ -134,7 +134,7 @@ The main function of a web framework is mapping each URL pattern to code (a corresponding :term:`route`. Our application uses the :meth:`pyramid.view.view_config` decorator to perform this mapping. -Open ``tutorial/tutorial/views/default.py`` in the ``views`` package. It +Open ``tutorial/views/default.py`` in the ``views`` package. It should already contain the following: .. literalinclude:: src/basiclayout/tutorial/views/default.py @@ -179,7 +179,7 @@ In a SQLAlchemy-based application, a *model* object is an object composed by querying the SQL database. The ``models`` package is where the ``alchemy`` scaffold put the classes that implement our models. -First, open ``tutorial/tutorial/models/meta.py``, which should already contain +First, open ``tutorial/models/meta.py``, which should already contain the following: .. literalinclude:: src/basiclayout/tutorial/models/meta.py @@ -234,7 +234,7 @@ The ``MyModel`` class has a ``__tablename__`` attribute. This informs SQLAlchemy which table to use to store the data representing instances of this class. -Finally, open ``tutorial/tutorial/models/__init__.py``, which should already +Finally, open ``tutorial/models/__init__.py``, which should already contain the following: .. literalinclude:: src/basiclayout/tutorial/models/__init__.py diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index b38177d04..9b517994f 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -14,11 +14,12 @@ Edit ``mymodel.py`` There is nothing special about the filename ``mymodel.py``. A project may have many models throughout its codebase in arbitrarily named - files. Files implementing models often have ``model`` in their filenames - or they may live in a Python subpackage of your application package named - ``models`` (as we've done in this tutorial), but this is only by convention. + modules. Modules implementing models often have ``model`` in their + names or they may live in a Python subpackage of your application package + named ``models`` (as we've done in this tutorial), but this is only a + convention and not a requirement. -Open the ``tutorial/tutorial/models/mymodel.py`` file and edit it to look like +Open the ``tutorial/models/mymodel.py`` file and edit it to look like the following: .. literalinclude:: src/models/tutorial/models/mymodel.py @@ -59,7 +60,7 @@ Edit ``models/__init__.py`` Since we are using a package for our models, we also need to update our ``__init__.py`` file. -Open the ``tutorial/tutorial/models/__init__.py`` file and edit it to look like +Open the ``tutorial/models/__init__.py`` file and edit it to look like the following: .. literalinclude:: src/models/tutorial/models/__init__.py @@ -84,7 +85,7 @@ Since we've changed our model, we need to make changes to our to create a ``Page`` rather than a ``MyModel`` and add it to our ``DBSession``. -Open ``tutorial/tutorial/scripts/initializedb.py`` and edit it to look like +Open ``tutorial/scripts/initializedb.py`` and edit it to look like the following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 08fa8f16b..8660c2772 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -67,7 +67,7 @@ like this:: Adding view functions in ``views/default.py`` ============================================= -It's time for a major change. Open ``tutorial/tutorial/views/default.py`` and +It's time for a major change. Open ``tutorial/views/default.py`` and edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py @@ -243,7 +243,7 @@ as such. The ``view.jinja2`` template ---------------------------- -Create ``tutorial/tutorial/templates/view.jinja2`` and add the following +Create ``tutorial/templates/view.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 @@ -263,7 +263,7 @@ wiki page. It includes: The ``edit.jinja2`` template ---------------------------- -Create ``tutorial/tutorial/templates/edit.jinja2`` and add the following +Create ``tutorial/templates/edit.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/edit.jinja2 -- cgit v1.2.3 From 8d457153240be8158eb22c6204fc37196e52b654 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 13:58:21 -0600 Subject: reference addon links for pyramid_jinja2, pyramid_tm, zope.sqlalchemy and transaction --- docs/tutorials/wiki2/installation.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 047c66c06..70d0444b7 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -397,12 +397,17 @@ Decisions the ``alchemy`` scaffold has made for you Creating a project using the ``alchemy`` scaffold makes the following assumptions: -- you are willing to use :term:`SQLAlchemy` as a database access tool +- You are willing to use :term:`SQLAlchemy` as a database access tool. -- you are willing to use :term:`URL dispatch` to map URLs to code +- You are willing to use :term:`URL dispatch` to map URLs to code. -- you want to use ``zope.sqlalchemy`` and ``pyramid_tm`` to scope - sessions to requests +- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package + to scope sessions to requests. + +- You want to use pyramid_jinja2_ to render your templates. + Different templating engines can be used but we had to choose one to + make the tutorial. See :ref:`available_template_system_bindings` for some + options. .. note:: @@ -411,3 +416,15 @@ assumptions: mechanism to map URLs to code (:term:`traversal`). However, for the purposes of this tutorial, we'll only be using URL dispatch and SQLAlchemy. + +.. _pyramid_jinja2: + http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ + +.. _pyramid_tm: + http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/ + +.. _zope.sqlalchemy: + https://pypi.python.org/pypi/zope.sqlalchemy + +.. _transaction: + http://zodb.readthedocs.org/en/latest/transactions.html -- cgit v1.2.3 From d6243ac1e7724cce26a738de5b86f187ef444e77 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 15:29:17 -0600 Subject: update definingmodels chapter of wiki2 tutorial --- docs/tutorials/wiki2/definingmodels.rst | 32 +++++----- docs/tutorials/wiki2/installation.rst | 7 +++ docs/tutorials/wiki2/src/models/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/models/production.ini | 2 - .../wiki2/src/models/tutorial/__init__.py | 2 +- .../wiki2/src/models/tutorial/models/__init__.py | 71 +++++++++++++++++++++- .../wiki2/src/models/tutorial/models/meta.py | 33 ---------- .../wiki2/src/models/tutorial/models/mymodel.py | 3 +- .../src/models/tutorial/scripts/initializedb.py | 27 ++++---- .../wiki2/src/models/tutorial/templates/404.jinja2 | 8 +++ docs/tutorials/wiki2/src/models/tutorial/tests.py | 18 +++--- .../wiki2/src/models/tutorial/views/default.py | 4 +- .../wiki2/src/models/tutorial/views/errors.py | 5 ++ 13 files changed, 133 insertions(+), 81 deletions(-) create mode 100644 docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/models/tutorial/views/errors.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 9b517994f..b90bf77e6 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -3,7 +3,7 @@ Defining the Domain Model ========================= The first change we'll make to our stock ``pcreate``-generated application will -be to define a :term:`domain model` constructor representing a wiki page. +be to define a wiki page :term:`domain model`. We'll do this inside our ``mymodel.py`` file. @@ -12,12 +12,12 @@ Edit ``mymodel.py`` .. note:: - There is nothing special about the filename ``mymodel.py``. A - project may have many models throughout its codebase in arbitrarily named - modules. Modules implementing models often have ``model`` in their - names or they may live in a Python subpackage of your application package - named ``models`` (as we've done in this tutorial), but this is only a - convention and not a requirement. + There is nothing special about the filename ``mymodel.py`` except that it + is a Python module. A project may have many models throughout its codebase + in arbitrarily named modules. Modules implementing models often have + ``model`` in their names or they may live in a Python subpackage of your + application package named ``models`` (as we've done in this tutorial), but + this is only a convention and not a requirement. Open the ``tutorial/models/mymodel.py`` file and edit it to look like the following: @@ -25,7 +25,7 @@ the following: .. literalinclude:: src/models/tutorial/models/mymodel.py :linenos: :language: py - :emphasize-lines: 9-11,13,14 + :emphasize-lines: 10-12,14-15 The highlighted lines are the ones that need to be changed, as well as removing lines that reference ``Index``. @@ -34,7 +34,7 @@ The first thing we've done is remove the stock ``MyModel`` class from the generated ``models.py`` file. The ``MyModel`` class is only a sample and we're not going to use it. -Then we added a ``Page`` class. Because this is an SQLAlchemy application, +Then we added a ``Page`` class. Because this is a SQLAlchemy application, this class inherits from an instance of :func:`sqlalchemy.ext.declarative.declarative_base`. @@ -43,7 +43,7 @@ this class inherits from an instance of :linenos: :language: python -As you can see, our ``Page`` class has a class level attribute +As you can see, our ``Page`` class has a class-level attribute ``__tablename__`` which equals the string ``'pages'``. This means that SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our ``Page`` class will also have class-level attributes named ``id``, ``name``, @@ -58,7 +58,7 @@ Edit ``models/__init__.py`` --------------------------- Since we are using a package for our models, we also need to update our -``__init__.py`` file. +``__init__.py`` file to ensure that the model is attached to the metadata. Open the ``tutorial/models/__init__.py`` file and edit it to look like the following: @@ -66,7 +66,7 @@ the following: .. literalinclude:: src/models/tutorial/models/__init__.py :linenos: :language: py - :emphasize-lines: 4 + :emphasize-lines: 8 Here we need to align our import with the name of the model ``Page``. @@ -83,7 +83,7 @@ Since we've changed our model, we need to make changes to our ``initializedb.py`` script. In particular, we'll replace our import of ``MyModel`` with one of ``Page`` and we'll change the very end of the script to create a ``Page`` rather than a ``MyModel`` and add it to our -``DBSession``. +``dbsession``. Open ``tutorial/scripts/initializedb.py`` and edit it to look like the following: @@ -91,11 +91,9 @@ the following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: :language: python - :emphasize-lines: 16,31,41 + :emphasize-lines: 18,44-45 -Only the highlighted lines need to be changed, as well as removing the lines -referencing ``pyramid.scripts.common`` and ``options`` under the ``main`` -function. +Only the highlighted lines need to be changed. Installing the project and re-initializing the database diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 70d0444b7..5d6d8e56b 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -298,6 +298,13 @@ Initializing the database We need to use the ``initialize_tutorial_db`` :term:`console script` to initialize our database. +.. note:: + + The ``initialize_tutorial_db`` command is not performing a migration but + rather simply creating missing tables and adding some dummy data. If you + already have a database, you should delete it before running + ``initialize_tutorial_db`` again. + Type the following command, making sure you are still in the ``tutorial`` directory (the directory with a ``development.ini`` in it): diff --git a/docs/tutorials/wiki2/src/models/MANIFEST.in b/docs/tutorials/wiki2/src/models/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/models/MANIFEST.in +++ b/docs/tutorials/wiki2/src/models/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/models/production.ini +++ b/docs/tutorials/wiki2/src/models/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py index 7994bbfa8..17763812a 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py @@ -6,7 +6,7 @@ def main(global_config, **settings): """ config = Configurator(settings=settings) config.include('pyramid_jinja2') - config.include('.models.meta') + config.include('.models') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py index 7b1c62867..4810c357a 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import Page # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py index 45571d78e..b23d0c0d2 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py @@ -1,10 +1,11 @@ -from .meta import Base from sqlalchemy import ( Column, Integer, Text, ) +from .meta import Base + class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py index 4aac4a848..601a6e73f 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -7,13 +7,15 @@ from pyramid.paster import ( setup_logging, ) -from ..models.meta import ( - Base, - get_session, +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import Page +from ..models import Page def usage(argv): @@ -27,16 +29,17 @@ def main(argv=sys.argv): if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + dbsession = get_tm_session(session_factory, transaction.manager) + + page = Page(name='FrontPage', data='This is the front page') + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py index b947e3bb1..c54945c28 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py @@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( - get_session, + from .models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py index 13ad8793c..ad0c728d7 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py @@ -3,7 +3,7 @@ from pyramid.view import view_config from sqlalchemy.exc import DBAPIError -from ..models.mymodel import MyModel +from ..models import MyModel @view_config(route_name='home', renderer='../templates/mytemplate.jinja2') @@ -12,7 +12,7 @@ def my_view(request): query = request.dbsession.query(MyModel) one = query.filter(MyModel.name == 'one').first() except DBAPIError: - return Response(db_err_msg, content_type='text/plain', status_int=500) + return Response(db_err_msg, content_type='text/plain', status=500) return {'one': one, 'project': 'tutorial'} diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/errors.py b/docs/tutorials/wiki2/src/models/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 4b23c9f1344a359214455668741b52c3db8cf6ea Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 22:08:52 -0600 Subject: update definingviews chapter of wiki2 tutorial --- docs/tutorials/wiki2/definingviews.rst | 29 +++++---- docs/tutorials/wiki2/src/views/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/views/production.ini | 2 - .../tutorials/wiki2/src/views/tutorial/__init__.py | 2 +- .../wiki2/src/views/tutorial/models/__init__.py | 71 +++++++++++++++++++++- .../wiki2/src/views/tutorial/models/meta.py | 33 ---------- .../wiki2/src/views/tutorial/models/mymodel.py | 3 +- .../src/views/tutorial/scripts/initializedb.py | 27 ++++---- .../wiki2/src/views/tutorial/templates/404.jinja2 | 8 +++ .../wiki2/src/views/tutorial/templates/edit.jinja2 | 2 +- .../wiki2/src/views/tutorial/templates/view.jinja2 | 2 +- docs/tutorials/wiki2/src/views/tutorial/tests.py | 18 +++--- .../wiki2/src/views/tutorial/views/default.py | 23 ++++--- .../wiki2/src/views/tutorial/views/errors.py | 5 ++ 14 files changed, 138 insertions(+), 89 deletions(-) create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/errors.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 8660c2772..4bc7f461b 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -96,11 +96,12 @@ We'll describe each one briefly in the following sections. .. note:: - There is nothing special about the filename ``default.py``. A project may - have many view callables throughout its codebase in arbitrarily named files. - Files implementing view callables often have ``view`` in their filenames (or + There is nothing special about the filename ``default.py`` exept that + it is a Python module. A project may have many view callables throughout + its codebase in arbitrarily named modules. + Modules implementing view callables often have ``view`` in their name (or may live in a Python subpackage of your application package named ``views``, - as in our case), but this is only by convention. + as in our case), but this is only by convention, not a requirement. The ``view_wiki`` view function ------------------------------- @@ -109,7 +110,7 @@ Following is the code for the ``view_wiki`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py :lines: 17-20 - :lineno-start: 17 + :lineno-match: :linenos: :language: python @@ -119,8 +120,8 @@ represents the path to our "FrontPage". The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the :class:`pyramid.interfaces.IResponse` interface, like +:class:`pyramid.httpexceptions.HTTPFound` class (instances of which +implement the :class:`pyramid.interfaces.IResponse` interface, like :class:`pyramid.response.Response` does). It uses the :meth:`pyramid.request.Request.route_url` API to construct an URL to the ``FrontPage`` page (i.e., ``http://localhost:6543/FrontPage``), and uses it as @@ -133,7 +134,7 @@ Here is the code for the ``view_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py :lines: 22-42 - :lineno-start: 22 + :lineno-match: :linenos: :language: python @@ -159,7 +160,7 @@ template, and we return a dictionary with a number of arguments. The fact that ``view_page()`` returns a dictionary (as opposed to a :term:`response` object) is a cue to :app:`Pyramid` that it should try to use a :term:`renderer` associated with the view configuration to render a response. In our case, the -renderer used will be the ``templates/view.jinja2`` template, as indicated in +renderer used will be the ``view.jinja2`` template, as indicated in the ``@view_config`` decorator that is applied to ``view_page()``. The ``add_page`` view function @@ -169,7 +170,7 @@ Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py :lines: 44-55 - :lineno-start: 44 + :lineno-match: :linenos: :language: python @@ -209,8 +210,8 @@ 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: 57-69 - :lineno-start: 57 + :lines: 57-68 + :lineno-match: :linenos: :language: python @@ -281,7 +282,9 @@ editing a wiki page. It displays a page containing a form that includes: The form POSTs back to the ``save_url`` argument supplied by the view (line 42). The view will use the ``body`` and ``form.submitted`` values. -.. note:: Our templates use a ``request`` object that none of our tutorial +.. note:: + + Our templates use a ``request`` object that none of our tutorial views return in their dictionary. ``request`` is one of several names that are available "by default" in a template when a template renderer is used. See :ref:`renderer_system_values` for information about other names that diff --git a/docs/tutorials/wiki2/src/views/MANIFEST.in b/docs/tutorials/wiki2/src/views/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/views/MANIFEST.in +++ b/docs/tutorials/wiki2/src/views/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/views/production.ini +++ b/docs/tutorials/wiki2/src/views/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index d28f09ca4..5d8c7fba2 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -6,7 +6,7 @@ def main(global_config, **settings): """ config = Configurator(settings=settings) config.include('pyramid_jinja2') - config.include('.models.meta') + config.include('.models') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('view_page', '/{pagename}') diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py index 7b1c62867..4810c357a 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import Page # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py index 45571d78e..b23d0c0d2 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py @@ -1,10 +1,11 @@ -from .meta import Base from sqlalchemy import ( Column, Integer, Text, ) +from .meta import Base + class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py index 4aac4a848..601a6e73f 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py @@ -7,13 +7,15 @@ from pyramid.paster import ( setup_logging, ) -from ..models.meta import ( - Base, - get_session, +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import Page +from ..models import Page def usage(argv): @@ -27,16 +29,17 @@ def main(argv=sys.argv): if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + dbsession = get_tm_session(session_factory, transaction.manager) + + page = Page(name='FrontPage', data='This is the front page') + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 index b3aadfc2e..a41d232e5 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -37,7 +37,7 @@ Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 index 36bb96870..fa09baf70 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -43,7 +43,7 @@ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py index b947e3bb1..c54945c28 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py @@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( - get_session, + from .models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index 3e5c61a72..96df85a97 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -9,17 +9,17 @@ from pyramid.httpexceptions import ( from pyramid.view import view_config -from ..models.mymodel import Page +from ..models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @view_config(route_name='view_wiki') def view_wiki(request): - return HTTPFound(location=request.route_url('view_page', - pagename='FrontPage')) + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) -@view_config(route_name='view_page', renderer='templates/view.jinja2') +@view_config(route_name='view_page', renderer='../templates/view.jinja2') def view_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).first() @@ -41,29 +41,28 @@ def view_page(request): edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='templates/edit.jinja2') +@view_config(route_name='add_page', renderer='../templates/edit.jinja2') def add_page(request): pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) page = Page(name='', data='') return dict(page=page, save_url=save_url) -@view_config(route_name='edit_page', renderer='templates/edit.jinja2') +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2') def edit_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), + save_url=request.route_url('edit_page', pagename=pagename), ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/errors.py b/docs/tutorials/wiki2/src/views/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 14cff75aca9c2858d0575d8e6beba9758eb012d6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 23:39:33 -0600 Subject: update authorization chapter of wiki2 tutorial --- docs/tutorials/wiki2/authorization.rst | 115 +++++++-------------- docs/tutorials/wiki2/src/authorization/MANIFEST.in | 2 +- .../wiki2/src/authorization/production.ini | 2 - .../wiki2/src/authorization/tutorial/__init__.py | 11 +- .../src/authorization/tutorial/models/__init__.py | 71 ++++++++++++- .../src/authorization/tutorial/models/meta.py | 33 ------ .../src/authorization/tutorial/models/mymodel.py | 19 ++-- .../authorization/tutorial/scripts/initializedb.py | 27 ++--- .../authorization/tutorial/security/__init__.py | 1 - .../src/authorization/tutorial/security/default.py | 11 +- .../authorization/tutorial/templates/404.jinja2 | 8 ++ .../authorization/tutorial/templates/edit.jinja2 | 6 +- .../authorization/tutorial/templates/view.jinja2 | 6 +- .../wiki2/src/authorization/tutorial/tests.py | 18 ++-- .../src/authorization/tutorial/views/default.py | 54 +++++----- .../src/authorization/tutorial/views/errors.py | 5 + 16 files changed, 200 insertions(+), 189 deletions(-) create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index e40433497..1ee5cc714 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -42,7 +42,7 @@ Access control Add users and groups ~~~~~~~~~~~~~~~~~~~~ -Create a new ``tutorial/tutorial/security/default.py`` subpackage with the +Create a new ``tutorial/security/default.py`` subpackage with the following content: .. literalinclude:: src/authorization/tutorial/security/default.py @@ -68,21 +68,17 @@ database, but here we use "dummy" data to represent user and groups sources. Add an ACL ~~~~~~~~~~ -Open ``tutorial/tutorial/models/mymodel.py`` and add the following import -statement just after the ``Base`` import at the top: +Open ``tutorial/models/mymodel.py`` and add the following import +statement at the top: .. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 3-6 - :linenos: - :lineno-start: 3 + :lines: 1-4 :language: python Add the following class definition at the end: .. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 22-26 - :linenos: - :lineno-start: 22 + :lines: 22-29 :language: python We import :data:`~pyramid.security.Allow`, an action that means that @@ -100,13 +96,13 @@ need to associate it to our :app:`Pyramid` application, so the ACL is provided to each view in the :term:`context` of the request as the ``context`` attribute. -Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to -our :term:`Configurator` constructor, that points to the class we created -above: +Open ``tutorial/__init__.py`` and define a new root factory using +:meth:`pyramid.config.Configurator.set_root_factory` using the class that we +created above: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 13-14 - :emphasize-lines: 2 + :lines: 14-17 + :emphasize-lines: 17 :language: python Only the highlighted line needs to be added. @@ -122,22 +118,19 @@ for more information about what an :term:`ACL` represents. Add authentication and authorization policies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/__init__.py`` and add the highlighted import +Open ``tutorial/__init__.py`` and add the highlighted import statements: .. literalinclude:: src/authorization/tutorial/__init__.py :lines: 1-5 - :linenos: :emphasize-lines: 2-5 :language: python Now add those policies to the configuration: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 7-16 - :linenos: - :lineno-start: 7 - :emphasize-lines: 4-6,9-10 + :lines: 11-19 + :emphasize-lines: 1-3,8-9 :language: python Only the highlighted lines need to be added. @@ -157,17 +150,17 @@ machinery represented by this policy; it is required. The ``callback`` is the Add permission declarations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/views/default.py`` and add a ``permission='view'`` +Open ``tutorial/views/default.py`` and add a ``permission='view'`` parameter to the ``@view_config`` decorator for ``view_wiki()`` and ``view_page()`` as follows: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 27-29 - :emphasize-lines: 1-2 + :lines: 24-25 + :emphasize-lines: 1 :language: python .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 33-35 + :lines: 29-31 :emphasize-lines: 1-2 :language: python @@ -180,12 +173,12 @@ Add a ``permission='edit'`` parameter to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 57-59 + :lines: 52-54 :emphasize-lines: 1-2 :language: python .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 72-74 + :lines: 66-68 :emphasize-lines: 1-2 :language: python @@ -203,11 +196,11 @@ Login, logout Add routes for /login and /logout ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Go back to ``tutorial/tutorial/__init__.py`` and add these two routes as +Go back to ``tutorial/__init__.py`` and add these two routes as highlighted: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 20-23 + :lines: 21-24 :emphasize-lines: 2-3 :language: python @@ -215,7 +208,7 @@ highlighted: ``view_page`` route definition: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 23 + :lines: 24 :language: python This is because ``view_page``'s route definition uses a catch-all @@ -235,12 +228,12 @@ We'll also add a ``logout`` view callable to our application and provide a link to it. This view will clear the credentials of the logged in user and redirect back to the front page. -Add the following import statements to ``tutorial/tutorial/views/default.py`` +Add the following import statements to ``tutorial/views/default.py`` after the import from ``pyramid.httpexceptions``: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 10-20 - :emphasize-lines: 1-11 + :lines: 9-19 + :emphasize-lines: 1-8,11 :language: python All the highlighted lines need to be added or edited. @@ -253,7 +246,7 @@ cookie. Now add the ``login`` and ``logout`` views at the end of the file: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 88-121 + :lines: 81-112 :language: python ``login()`` has two decorators: @@ -274,7 +267,7 @@ it with the ``logout`` route. It will be invoked when we visit ``/logout``. Add the ``login.jinja2`` template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create ``tutorial/tutorial/templates/login.jinja2`` with the following content: +Create ``tutorial/templates/login.jinja2`` with the following content: .. literalinclude:: src/authorization/tutorial/templates/login.jinja2 :language: html @@ -282,38 +275,11 @@ Create ``tutorial/tutorial/templates/login.jinja2`` with the following content: The above template is referenced in the login view that we just added in ``views/default.py``. -Return a ``logged_in`` flag to the renderer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Open ``tutorial/tutorial/views/default.py`` again. Add a ``logged_in`` -parameter to the return value of ``view_page()``, ``add_page()``, and -``edit_page()`` as follows: - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 54-55 - :emphasize-lines: 1-2 - :language: python - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 69-70 - :emphasize-lines: 1-2 - :language: python - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 82-86 - :emphasize-lines: 3-4 - :language: python - -Only the highlighted lines need to be added or edited. - -The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if -the user is not authenticated, or a userid if the user is authenticated. - Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/templates/edit.jinja2`` and -``tutorial/tutorial/templates/view.jinja2`` and add the following code as +Open ``tutorial/templates/edit.jinja2`` and +``tutorial/templates/view.jinja2`` and add the following code as indicated by the highlighted lines. .. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 @@ -321,42 +287,41 @@ indicated by the highlighted lines. :emphasize-lines: 3-7 :language: html -The attribute ``logged_in`` will make the element be included when -``logged_in`` is any user id. The link will invoke the logout view. The above -element will not be included if ``logged_in`` is ``None``, such as when a user -is not authenticated. +The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if +the user is not authenticated, or a userid if the user is authenticated. This +check will make the logout link active only when the user is logged in. Reviewing our changes --------------------- -Our ``tutorial/tutorial/__init__.py`` will look like this when we're done: +Our ``tutorial/__init__.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/__init__.py :linenos: - :emphasize-lines: 2-3,5,10-12,14-16,21-22 + :emphasize-lines: 2-3,5,11-13,17-19,22-23 :language: python Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/models/mymodel.py`` will look like this when we're done: +Our ``tutorial/models/mymodel.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/models/mymodel.py :linenos: - :emphasize-lines: 3-6,22-26 + :emphasize-lines: 1-4,22-29 :language: python Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/views/default.py`` will look like this when we're done: +Our ``tutorial/views/default.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/views/default.py :linenos: - :emphasize-lines: 10-20,27-28,33-34,54-55,57-58,69-70,72-73,84-85,88-121 + :emphasize-lines: 9-16,19,24,29-30,52-53,66-67,81-112 :language: python Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/templates/edit.jinja2`` template will look like this when +Our ``tutorial/templates/edit.jinja2`` template will look like this when we're done: .. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 @@ -366,7 +331,7 @@ we're done: Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/templates/view.jinja2`` template will look like this when +Our ``tutorial/templates/view.jinja2`` template will look like this when we're done: .. literalinclude:: src/authorization/tutorial/templates/view.jinja2 diff --git a/docs/tutorials/wiki2/src/authorization/MANIFEST.in b/docs/tutorials/wiki2/src/authorization/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/authorization/MANIFEST.in +++ b/docs/tutorials/wiki2/src/authorization/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 084fee19f..a62c42378 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -2,7 +2,8 @@ from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from security.default import groupfinder +from .security.default import groupfinder + def main(global_config, **settings): """ This function returns a Pyramid WSGI application. @@ -10,12 +11,12 @@ def main(global_config, **settings): authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() - config = Configurator(settings=settings, - root_factory='tutorial.models.mymodel.RootFactory') + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.set_root_factory('.models.mymodel.RootFactory') config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) - config.include('pyramid_jinja2') - config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py index 7b1c62867..4810c357a 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import Page # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py index 03e2f90ca..25209c745 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py @@ -1,15 +1,14 @@ -from .meta import Base - from pyramid.security import ( Allow, Everyone, - ) - +) from sqlalchemy import ( Column, Integer, Text, - ) +) + +from .meta import Base class Page(Base): @@ -19,8 +18,12 @@ class Page(Base): name = Column(Text, unique=True) data = Column(Integer) + class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit'), + ] + def __init__(self, request): - pass \ No newline at end of file + pass diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py index 4aac4a848..601a6e73f 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py @@ -7,13 +7,15 @@ from pyramid.paster import ( setup_logging, ) -from ..models.meta import ( - Base, - get_session, +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import Page +from ..models import Page def usage(argv): @@ -27,16 +29,17 @@ def main(argv=sys.argv): if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + dbsession = get_tm_session(session_factory, transaction.manager) + + page = Page(name='FrontPage', data='This is the front page') + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py index 5bb534f79..e69de29bb 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py @@ -1 +0,0 @@ -# package diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py index d88c9c71f..7fc1ea7c8 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py @@ -1,6 +1,11 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} -GROUPS = {'editor':['group:editors']} +USERS = { + 'editor': 'editor', + 'viewer': 'viewer', +} + +GROUPS = { + 'editor': ['group:editors'], +} def groupfinder(userid, request): if userid in USERS: diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 index c4f3a2c93..70ce49b73 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -33,16 +33,16 @@
- {% if logged_in %} + {% if request.authenticated_userid is not None %}

- Logout + Logout

{% endif %}

Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 index a7afc66fc..b12ca5b0c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -33,9 +33,9 @@
- {% if logged_in %} + {% if request.authenticated_userid is not None %}

- Logout + Logout

{% endif %}

{{ content|safe }}

@@ -48,7 +48,7 @@ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py index b947e3bb1..c54945c28 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py @@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( - get_session, + from .models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index f35f041a4..aa77facd7 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -6,31 +6,27 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) - from pyramid.view import ( view_config, forbidden_view_config, ) - from pyramid.security import ( remember, forget, ) +from ..models import Page from ..security.default import USERS -from ..models.mymodel import Page - # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki', - permission='view') +@view_config(route_name='view_wiki', permission='view') def view_wiki(request): - return HTTPFound(location=request.route_url('view_page', - pagename='FrontPage')) + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) -@view_config(route_name='view_page', renderer='templates/view.jinja2', +@view_config(route_name='view_page', renderer='../templates/view.jinja2', permission='view') def view_page(request): pagename = request.matchdict['pagename'] @@ -51,10 +47,9 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=request.authenticated_userid) + return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='templates/edit.jinja2', +@view_config(route_name='add_page', renderer='../templates/edit.jinja2', permission='edit') def add_page(request): pagename = request.matchdict['pagename'] @@ -62,29 +57,27 @@ def add_page(request): body = request.params['body'] page = Page(name=pagename, data=body) request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) page = Page(name='', data='') - return dict(page=page, save_url=save_url, - logged_in=request.authenticated_userid) + return dict(page=page, save_url=save_url) -@view_config(route_name='edit_page', renderer='templates/edit.jinja2', +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', permission='edit') def edit_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid + save_url=request.route_url('edit_page', pagename=pagename), ) + @view_config(route_name='login', renderer='templates/login.jinja2') @forbidden_view_config(renderer='templates/login.jinja2') def login(request): @@ -101,20 +94,19 @@ def login(request): password = request.params['password'] if USERS.get(login) == password: headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) + return HTTPFound(location=came_from, headers=headers) message = 'Failed login' return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, + message=message, + url=request.route_url('login'), + came_from=came_from, + login=login, + password=password, ) @view_config(route_name='logout') def logout(request): headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 4337a25b30f53ad8f64babe5835a4d3d35f29a41 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 00:00:23 -0600 Subject: minor fixes to wiki2 distributing chapter --- docs/tutorials/wiki2/distributing.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst index fee50a1cf..ec90859a9 100644 --- a/docs/tutorials/wiki2/distributing.rst +++ b/docs/tutorials/wiki2/distributing.rst @@ -4,9 +4,8 @@ Distributing Your Application Once your application works properly, you can create a "tarball" from it by using the ``setup.py sdist`` command. The following commands assume your -current working directory is the ``tutorial`` package we've created and that -the parent directory of the ``tutorial`` package is a virtualenv representing -a :app:`Pyramid` environment. +current working directory contains the ``tutorial`` package and the +``setup.py`` file. On UNIX: -- cgit v1.2.3 From 0b02e46ff9dafcdf9d4c03bac2958c8b20c596f6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 00:19:31 -0600 Subject: expose the session factory on the registry --- docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py | 1 + docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py | 1 + docs/tutorials/wiki2/src/models/tutorial/models/__init__.py | 1 + 3 files changed, 3 insertions(+) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py index 4810c357a..3d3efe06f 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py index a4026fcd6..48a957ecb 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py index 4810c357a..3d3efe06f 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( -- cgit v1.2.3 From 1c108019dae884e810d6436e10f8648c77bdd181 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 00:43:47 -0600 Subject: [wip] update tests in wiki2 tutorial --- docs/tutorials/wiki2/src/tests/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/tests/production.ini | 2 - docs/tutorials/wiki2/src/tests/setup.py | 1 - .../tutorials/wiki2/src/tests/tutorial/__init__.py | 11 ++-- .../wiki2/src/tests/tutorial/models/__init__.py | 72 +++++++++++++++++++++- .../wiki2/src/tests/tutorial/models/meta.py | 33 ---------- .../wiki2/src/tests/tutorial/models/mymodel.py | 19 +++--- .../src/tests/tutorial/scripts/initializedb.py | 27 ++++---- .../wiki2/src/tests/tutorial/security/__init__.py | 1 - .../wiki2/src/tests/tutorial/security/default.py | 11 +++- .../wiki2/src/tests/tutorial/templates/404.jinja2 | 8 +++ .../wiki2/src/tests/tutorial/templates/edit.jinja2 | 6 +- .../wiki2/src/tests/tutorial/templates/view.jinja2 | 6 +- .../wiki2/src/tests/tutorial/tests/__init__.py | 1 - .../src/tests/tutorial/tests/test_functional.py | 29 ++------- .../wiki2/src/tests/tutorial/tests/test_views.py | 46 +++----------- .../wiki2/src/tests/tutorial/views/default.py | 54 +++++++--------- .../wiki2/src/tests/tutorial/views/errors.py | 5 ++ .../wiki2/src/views/tutorial/models/__init__.py | 1 + docs/tutorials/wiki2/tests.rst | 8 +++ 20 files changed, 176 insertions(+), 167 deletions(-) create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/errors.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/tests/MANIFEST.in b/docs/tutorials/wiki2/src/tests/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/tests/MANIFEST.in +++ b/docs/tutorials/wiki2/src/tests/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index f640b4399..d4e5a4072 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -18,7 +18,6 @@ requires = [ 'zope.sqlalchemy', 'waitress', 'docutils', - 'WebTest', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py index 084fee19f..a62c42378 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -2,7 +2,8 @@ from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from security.default import groupfinder +from .security.default import groupfinder + def main(global_config, **settings): """ This function returns a Pyramid WSGI application. @@ -10,12 +11,12 @@ def main(global_config, **settings): authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() - config = Configurator(settings=settings, - root_factory='tutorial.models.mymodel.RootFactory') + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.set_root_factory('.models.mymodel.RootFactory') config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) - config.include('pyramid_jinja2') - config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py index 7b1c62867..3d3efe06f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py @@ -1,7 +1,73 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import Page # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py index 03e2f90ca..25209c745 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py @@ -1,15 +1,14 @@ -from .meta import Base - from pyramid.security import ( Allow, Everyone, - ) - +) from sqlalchemy import ( Column, Integer, Text, - ) +) + +from .meta import Base class Page(Base): @@ -19,8 +18,12 @@ class Page(Base): name = Column(Text, unique=True) data = Column(Integer) + class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit'), + ] + def __init__(self, request): - pass \ No newline at end of file + pass diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py index 4aac4a848..601a6e73f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -7,13 +7,15 @@ from pyramid.paster import ( setup_logging, ) -from ..models.meta import ( - Base, - get_session, +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import Page +from ..models import Page def usage(argv): @@ -27,16 +29,17 @@ def main(argv=sys.argv): if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + dbsession = get_tm_session(session_factory, transaction.manager) + + page = Page(name='FrontPage', data='This is the front page') + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py index 5bb534f79..e69de29bb 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py @@ -1 +0,0 @@ -# package diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py index d88c9c71f..7fc1ea7c8 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py @@ -1,6 +1,11 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} -GROUPS = {'editor':['group:editors']} +USERS = { + 'editor': 'editor', + 'viewer': 'viewer', +} + +GROUPS = { + 'editor': ['group:editors'], +} def groupfinder(userid, request): if userid in USERS: diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 index c4f3a2c93..70ce49b73 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -33,16 +33,16 @@
- {% if logged_in %} + {% if request.authenticated_userid is not None %}

- Logout + Logout

{% endif %}

Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 index a7afc66fc..b12ca5b0c 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -33,9 +33,9 @@
- {% if logged_in %} + {% if request.authenticated_userid is not None %}

- Logout + Logout

{% endif %}

{{ content|safe }}

@@ -48,7 +48,7 @@ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py index 8b1378917..e69de29bb 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index 339c60bc2..eda47c064 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -1,17 +1,5 @@ import unittest -from pyramid import testing - - -def dummy_request(dbsession): - return testing.DummyRequest(dbsession=dbsession) - - -def _register_routes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - class FunctionalTests(unittest.TestCase): @@ -27,11 +15,8 @@ class FunctionalTests(unittest.TestCase): @staticmethod def setup_database(): import transaction - from tutorial.models.mymodel import Page - from tutorial.models.meta import ( - Base, - ) - import tutorial.models.meta + from tutorial.models import Page + from tutorial.models.meta import Base def initialize_db(dbsession, engine): @@ -40,11 +25,9 @@ class FunctionalTests(unittest.TestCase): model = Page(name='FrontPage', data='This is the front page') dbsession.add(model) - def wrap_get_session(transaction_manager, dbmaker): - dbsession = get_session(transaction_manager, dbmaker) + def wrap_get_tm_session(session_factory, transaction_manager): + dbsession = get_tm_session(session_factory, transaction_manager) initialize_db(dbsession, engine) - tutorial.models.meta.get_session = get_session - tutorial.models.meta.get_engine = get_engine return dbsession def wrap_get_engine(settings): @@ -53,10 +36,10 @@ class FunctionalTests(unittest.TestCase): return engine get_session = tutorial.models.meta.get_session - tutorial.models.meta.get_session = wrap_get_session + tutorial.models.get_tm_session = wrap_get_tm_session get_engine = tutorial.models.meta.get_engine - tutorial.models.meta.get_engine = wrap_get_engine + tutorial.models.get_engine = wrap_get_engine @classmethod def setUpClass(cls): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py index d70311e38..81d84fa30 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -10,35 +10,29 @@ def dummy_request(dbsession): def _register_routes(config): config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') config.add_route('add_page', 'add_page/{pagename}') + config.add_route('edit_page', '{pagename}/edit_page') class BaseTest(unittest.TestCase): def setUp(self): + from ..models import get_tm_session self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('..models.meta') - _register_routes(self.config) - settings = self.config.get_settings() + self.config.include('..models') + self.config.include(_register_routes) - from ..models.meta import ( - get_session, - get_engine, - get_dbmaker, - ) - - self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) - - self.session = get_session(transaction.manager, dbmaker) + session_factory = self.config.registry['dbsession_factory'] + self.session = get_tm_session(session_factory, transaction.manager) self.init_database() def init_database(self): from ..models.meta import Base - Base.metadata.create_all(self.engine) + session_factory = self.config.registry['dbsession_factory'] + engine = session_factory.get_bind() + Base.metadata.create_all(engine) def tearDown(self): testing.tearDown() @@ -46,7 +40,6 @@ class BaseTest(unittest.TestCase): class ViewWikiTests(unittest.TestCase): - def setUp(self): self.config = testing.setUp() _register_routes(self.config) @@ -65,13 +58,6 @@ class ViewWikiTests(unittest.TestCase): class ViewPageTests(BaseTest): - def setUp(self): - super(ViewPageTests, self).setUp() - - def tearDown(self): - transaction.abort() - testing.tearDown() - def _callFUT(self, request): from tutorial.views.default import view_page return view_page(request) @@ -102,13 +88,6 @@ class ViewPageTests(BaseTest): class AddPageTests(BaseTest): - def setUp(self): - super(AddPageTests, self).setUp() - - def tearDown(self): - transaction.abort() - testing.tearDown() - def _callFUT(self, request): from tutorial.views.default import add_page return add_page(request) @@ -133,13 +112,6 @@ class AddPageTests(BaseTest): class EditPageTests(BaseTest): - def setUp(self): - super(EditPageTests, self).setUp() - - def tearDown(self): - transaction.abort() - testing.tearDown() - def _callFUT(self, request): from tutorial.views.default import edit_page return edit_page(request) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index f35f041a4..aa77facd7 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -6,31 +6,27 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) - from pyramid.view import ( view_config, forbidden_view_config, ) - from pyramid.security import ( remember, forget, ) +from ..models import Page from ..security.default import USERS -from ..models.mymodel import Page - # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki', - permission='view') +@view_config(route_name='view_wiki', permission='view') def view_wiki(request): - return HTTPFound(location=request.route_url('view_page', - pagename='FrontPage')) + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) -@view_config(route_name='view_page', renderer='templates/view.jinja2', +@view_config(route_name='view_page', renderer='../templates/view.jinja2', permission='view') def view_page(request): pagename = request.matchdict['pagename'] @@ -51,10 +47,9 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=request.authenticated_userid) + return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='templates/edit.jinja2', +@view_config(route_name='add_page', renderer='../templates/edit.jinja2', permission='edit') def add_page(request): pagename = request.matchdict['pagename'] @@ -62,29 +57,27 @@ def add_page(request): body = request.params['body'] page = Page(name=pagename, data=body) request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) page = Page(name='', data='') - return dict(page=page, save_url=save_url, - logged_in=request.authenticated_userid) + return dict(page=page, save_url=save_url) -@view_config(route_name='edit_page', renderer='templates/edit.jinja2', +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', permission='edit') def edit_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid + save_url=request.route_url('edit_page', pagename=pagename), ) + @view_config(route_name='login', renderer='templates/login.jinja2') @forbidden_view_config(renderer='templates/login.jinja2') def login(request): @@ -101,20 +94,19 @@ def login(request): password = request.params['password'] if USERS.get(login) == password: headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) + return HTTPFound(location=came_from, headers=headers) message = 'Failed login' return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, + message=message, + url=request.route_url('login'), + came_from=came_from, + login=login, + password=password, ) @view_config(route_name='logout') def logout(request): headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py index 4810c357a..3d3efe06f 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index fe3fdaf2c..a99cd68cc 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -18,6 +18,14 @@ subpackage, and add several new tests. Start by creating a new directory and a new empty file ``tests/__init__.py``. +.. warning:: + + It is very important when refactoring a Python module into a package to + be sure to delete the cache files (``.pyc`` files or ``__pycache__`` + folders) sitting around! Python will prioritize the cache files before + traversing into folders and so it will use the old code and you will wonder + why none of your changes are working! + Test the views ============== -- cgit v1.2.3 From d6a758e58ef1c4782ecd3fe53c8563284f2496ca Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 22:37:22 -0600 Subject: fix tests to get the bind from dbsession_factory properly --- docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py index 81d84fa30..b2830d070 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -31,7 +31,7 @@ class BaseTest(unittest.TestCase): def init_database(self): from ..models.meta import Base session_factory = self.config.registry['dbsession_factory'] - engine = session_factory.get_bind() + engine = session_factory.kw['bind'] Base.metadata.create_all(engine) def tearDown(self): -- cgit v1.2.3 From 91ffccabafd2f074ac7620b5b64e52a8eb3cb31a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 23:00:48 -0600 Subject: fix jinja2 none test --- docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 | 2 +- docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 | 2 +- docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 | 2 +- docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 index 70ce49b73..4d767cfbe 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -33,7 +33,7 @@
- {% if request.authenticated_userid is not None %} + {% if request.authenticated_userid is not none %}

Logout

diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 index b12ca5b0c..942b8479b 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -33,7 +33,7 @@
- {% if request.authenticated_userid is not None %} + {% if request.authenticated_userid is not none %}

Logout

diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 index 70ce49b73..4d767cfbe 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -33,7 +33,7 @@
- {% if request.authenticated_userid is not None %} + {% if request.authenticated_userid is not none %}

Logout

diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 index b12ca5b0c..942b8479b 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -33,7 +33,7 @@
- {% if request.authenticated_userid is not None %} + {% if request.authenticated_userid is not none %}

Logout

-- cgit v1.2.3 From 62f0411a12a7f86bd7e3060b14f223cfb96322ad Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 23:00:58 -0600 Subject: fix functional tests --- docs/tutorials/wiki2/src/tests/setup.py | 5 ++ .../src/tests/tutorial/tests/test_functional.py | 57 +++++++--------------- 2 files changed, 23 insertions(+), 39 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index d4e5a4072..93195a68c 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -20,6 +20,10 @@ requires = [ 'docutils', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -39,6 +43,7 @@ setup(name='tutorial', zip_safe=False, test_suite='tutorial', install_requires=requires, + tests_require=tests_require, entry_points="""\ [paste.app_factory] main = tutorial:main diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index eda47c064..2c08f9a64 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -1,4 +1,6 @@ +import transaction import unittest +from webtest import TestApp class FunctionalTests(unittest.TestCase): @@ -10,55 +12,32 @@ class FunctionalTests(unittest.TestCase): editor_login = '/login?login=editor&password=editor' \ '&came_from=FrontPage&form.submitted=Login' - engine = None - - @staticmethod - def setup_database(): - import transaction - from tutorial.models import Page - from tutorial.models.meta import Base - - - def initialize_db(dbsession, engine): - Base.metadata.create_all(engine) - with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) - - def wrap_get_tm_session(session_factory, transaction_manager): - dbsession = get_tm_session(session_factory, transaction_manager) - initialize_db(dbsession, engine) - return dbsession - - def wrap_get_engine(settings): - global engine - engine = get_engine(settings) - return engine - - get_session = tutorial.models.meta.get_session - tutorial.models.get_tm_session = wrap_get_tm_session - - get_engine = tutorial.models.meta.get_engine - tutorial.models.get_engine = wrap_get_engine - @classmethod def setUpClass(cls): - cls.setup_database() - - from webtest import TestApp + from tutorial.models.meta import Base + from tutorial.models import ( + Page, + get_tm_session, + ) from tutorial import main + settings = {'sqlalchemy.url': 'sqlite://'} app = main({}, **settings) cls.testapp = TestApp(app) + session_factory = app.registry['dbsession_factory'] + cls.engine = session_factory.kw['bind'] + Base.metadata.create_all(bind=cls.engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + model = Page(name='FrontPage', data='This is the front page') + dbsession.add(model) + @classmethod def tearDownClass(cls): from tutorial.models.meta import Base - Base.metadata.drop_all(engine) - - def tearDown(self): - import transaction - transaction.abort() + Base.metadata.drop_all(bind=cls.engine) def test_root(self): res = self.testapp.get('/', status=302) -- cgit v1.2.3 From ff88c1535c65a717a030a480e39723724d53d985 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 10 Feb 2016 22:28:47 -0600 Subject: split login from forbidden --- .../wiki2/src/tests/tutorial/tests/test_functional.py | 8 ++++---- docs/tutorials/wiki2/src/tests/tutorial/views/default.py | 7 +------ docs/tutorials/wiki2/src/tests/tutorial/views/errors.py | 11 ++++++++++- 3 files changed, 15 insertions(+), 11 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index 2c08f9a64..b25d9a332 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -70,21 +70,21 @@ class FunctionalTests(unittest.TestCase): self.assertTrue(b'Logout' not in res.body) def test_anonymous_user_cannot_edit(self): - res = self.testapp.get('/FrontPage/edit_page', status=200) + res = self.testapp.get('/FrontPage/edit_page', status=302).follow() self.assertTrue(b'Login' in res.body) def test_anonymous_user_cannot_add(self): - res = self.testapp.get('/add_page/NewPage', status=200) + res = self.testapp.get('/add_page/NewPage', status=302).follow() self.assertTrue(b'Login' in res.body) def test_viewer_user_cannot_edit(self): self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/FrontPage/edit_page', status=200) + res = self.testapp.get('/FrontPage/edit_page', status=302).follow() self.assertTrue(b'Login' in res.body) def test_viewer_user_cannot_add(self): self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=200) + res = self.testapp.get('/add_page/NewPage', status=302).follow() self.assertTrue(b'Login' in res.body) def test_editors_member_user_can_edit(self): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index aa77facd7..bc8a59fe8 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -6,14 +6,11 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) -from pyramid.view import ( - view_config, - forbidden_view_config, - ) from pyramid.security import ( remember, forget, ) +from pyramid.view import view_config from ..models import Page from ..security.default import USERS @@ -77,9 +74,7 @@ def edit_page(request): save_url=request.route_url('edit_page', pagename=pagename), ) - @view_config(route_name='login', renderer='templates/login.jinja2') -@forbidden_view_config(renderer='templates/login.jinja2') def login(request): login_url = request.route_url('login') referrer = request.url diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py index a4b8201f1..f8dbe4e05 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py @@ -1,5 +1,14 @@ -from pyramid.view import notfound_view_config +from pyramid.httpexceptions import HTTPFound +from pyramid.view import ( + forbidden_view_config, + notfound_view_config, +) @notfound_view_config(renderer='../templates/404.jinja2') def notfound_view(request): return {} + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'came_from': request.url}) + return HTTPFound(location=next_url) -- cgit v1.2.3 From 07d38f5d4c9ebaf267d4ecaf8c0bd4c508f1848f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 10 Feb 2016 22:38:38 -0600 Subject: several simple refactorings - move auth from default.py to auth.py - rename errors to notfound - drop basic templates (mytemplate.jinja2, layout.jinja2) --- .../authorization/tutorial/templates/layout.jinja2 | 66 ---------------- .../tutorial/templates/mytemplate.jinja2 | 8 -- .../wiki2/src/authorization/tutorial/views/auth.py | 49 ++++++++++++ .../src/authorization/tutorial/views/default.py | 44 +---------- .../src/authorization/tutorial/views/errors.py | 5 -- .../src/authorization/tutorial/views/notfound.py | 7 ++ .../wiki2/src/basiclayout/tutorial/views/errors.py | 5 -- .../src/basiclayout/tutorial/views/notfound.py | 7 ++ .../wiki2/src/models/tutorial/views/errors.py | 5 -- .../wiki2/src/models/tutorial/views/notfound.py | 7 ++ .../src/tests/tutorial/templates/layout.jinja2 | 66 ---------------- .../src/tests/tutorial/templates/mytemplate.jinja2 | 8 -- .../src/tests/tutorial/tests/test_functional.py | 15 ++-- .../wiki2/src/tests/tutorial/views/auth.py | 49 ++++++++++++ .../wiki2/src/tests/tutorial/views/default.py | 37 --------- .../wiki2/src/tests/tutorial/views/errors.py | 14 ---- .../wiki2/src/tests/tutorial/views/notfound.py | 7 ++ .../wiki2/src/views/tutorial/templates/404.jinja2 | 2 +- .../wiki2/src/views/tutorial/templates/edit.jinja2 | 88 +++++----------------- .../src/views/tutorial/templates/layout.jinja2 | 19 +---- .../src/views/tutorial/templates/mytemplate.jinja2 | 8 -- .../wiki2/src/views/tutorial/templates/view.jinja2 | 82 ++++---------------- .../wiki2/src/views/tutorial/views/errors.py | 5 -- .../wiki2/src/views/tutorial/views/notfound.py | 7 ++ 24 files changed, 184 insertions(+), 426 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py delete mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py delete mode 100644 docs/tutorials/wiki2/src/models/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/views/notfound.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/auth.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/notfound.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 deleted file mode 100644 index ff624c65b..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
- {% block content %} -

No content

- {% endblock content %} -
-
-
- -
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 deleted file mode 100644 index bb622bf5a..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "layout.jinja2" %} - -{% block content %} -
-

Pyramid Alchemy scaffold

-

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

-
-{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py new file mode 100644 index 000000000..08aa2bfad --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py @@ -0,0 +1,49 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..security.default import USERS + + +@view_config(route_name='login', renderer='templates/login.jinja2') +def login(request): + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + came_from=came_from, + login=login, + password=password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'came_from': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index aa77facd7..6fb3c8744 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -6,17 +6,9 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) -from pyramid.view import ( - view_config, - forbidden_view_config, - ) -from pyramid.security import ( - remember, - forget, - ) +from pyramid.view import view_config from ..models import Page -from ..security.default import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -76,37 +68,3 @@ def edit_page(request): page=page, save_url=request.route_url('edit_page', pagename=pagename), ) - - -@view_config(route_name='login', renderer='templates/login.jinja2') -@forbidden_view_config(renderer='templates/login.jinja2') -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, headers=headers) - message = 'Failed login' - - return dict( - message=message, - url=request.route_url('login'), - came_from=came_from, - login=login, - password=password, - ) - -@view_config(route_name='logout') -def logout(request): - headers = forget(request) - next_url = request.route_url('view_wiki') - return HTTPFound(location=next_url, headers=headers) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py deleted file mode 100644 index a4b8201f1..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py deleted file mode 100644 index a4b8201f1..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/errors.py b/docs/tutorials/wiki2/src/models/tutorial/views/errors.py deleted file mode 100644 index a4b8201f1..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/views/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 deleted file mode 100644 index ff624c65b..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
- {% block content %} -

No content

- {% endblock content %} -
-
-
- -
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 deleted file mode 100644 index bb622bf5a..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "layout.jinja2" %} - -{% block content %} -
-

Pyramid Alchemy scaffold

-

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

-
-{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index b25d9a332..c716537ae 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -5,12 +5,15 @@ from webtest import TestApp class FunctionalTests(unittest.TestCase): - viewer_login = '/login?login=viewer&password=viewer' \ - '&came_from=FrontPage&form.submitted=Login' - viewer_wrong_login = '/login?login=viewer&password=incorrect' \ - '&came_from=FrontPage&form.submitted=Login' - editor_login = '/login?login=editor&password=editor' \ - '&came_from=FrontPage&form.submitted=Login' + viewer_login = ( + '/login?login=viewer&password=viewer' + '&came_from=FrontPage&form.submitted=Login') + viewer_wrong_login = ( + '/login?login=viewer&password=incorrect' + '&came_from=FrontPage&form.submitted=Login') + editor_login = ( + '/login?login=editor&password=editor' + '&came_from=FrontPage&form.submitted=Login') @classmethod def setUpClass(cls): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py new file mode 100644 index 000000000..08aa2bfad --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py @@ -0,0 +1,49 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..security.default import USERS + + +@view_config(route_name='login', renderer='templates/login.jinja2') +def login(request): + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + came_from=came_from, + login=login, + password=password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'came_from': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index bc8a59fe8..6fb3c8744 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -6,14 +6,9 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) -from pyramid.security import ( - remember, - forget, - ) from pyramid.view import view_config from ..models import Page -from ..security.default import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -73,35 +68,3 @@ def edit_page(request): page=page, save_url=request.route_url('edit_page', pagename=pagename), ) - -@view_config(route_name='login', renderer='templates/login.jinja2') -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, headers=headers) - message = 'Failed login' - - return dict( - message=message, - url=request.route_url('login'), - came_from=came_from, - login=login, - password=password, - ) - -@view_config(route_name='logout') -def logout(request): - headers = forget(request) - next_url = request.route_url('view_wiki') - return HTTPFound(location=next_url, headers=headers) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py deleted file mode 100644 index f8dbe4e05..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyramid.httpexceptions import HTTPFound -from pyramid.view import ( - forbidden_view_config, - notfound_view_config, -) - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} - -@forbidden_view_config() -def forbidden_view(request): - next_url = request.route_url('login', _query={'came_from': request.url}) - return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 index 1917f83c7..37b0a16b6 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 @@ -2,7 +2,7 @@ {% block content %}
-

Pyramid Alchemy scaffold

+

Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)

404 Page Not Found

{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 index a41d232e5..e47b3aabf 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -1,68 +1,20 @@ - - - - - - - - - - - Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

- -
- -
-
- -
- -
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block title %}Edit {{page.name}} - {% endblock title %} + +{% block content %} +

+Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the +FrontPage. +

+
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 index ff624c65b..68743e0df 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -8,7 +8,7 @@ - Alchemy Scaffold for The Pyramid Web Framework + {% block title %}{% if page.name %} {{page.name}} - {% endif %}{% endblock title %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) @@ -32,20 +32,9 @@
- {% block content %} -

No content

- {% endblock content %} -
-
-
-
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 deleted file mode 100644 index bb622bf5a..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "layout.jinja2" %} - -{% block content %} -
-

Pyramid Alchemy scaffold

-

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

-
-{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 index fa09baf70..c582ce1f9 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -1,66 +1,16 @@ - - - - - - - - - - - {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

{{ content|safe }}

-

- - Edit this page - -

-

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block content %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the +FrontPage. +

+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/errors.py b/docs/tutorials/wiki2/src/views/tutorial/views/errors.py deleted file mode 100644 index a4b8201f1..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/views/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} -- cgit v1.2.3 From e01b270c451ce6f23b53181ad79b430666ebe003 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 10 Feb 2016 23:45:32 -0600 Subject: explain the base layout.jinja2 template and notfound view --- docs/tutorials/wiki2/definingviews.rst | 81 +++++++++++++++++++--- .../wiki2/src/views/tutorial/views/default.py | 2 +- 2 files changed, 74 insertions(+), 9 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 4bc7f461b..6629839f8 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -73,7 +73,7 @@ edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py :linenos: :language: python - :emphasize-lines: 1-9,12-70 + :emphasize-lines: 1-9,12-68 The highlighted lines need to be added or edited. @@ -241,6 +241,26 @@ These templates will live in the ``templates`` directory of our tutorial package. Jinja2 templates must have a ``.jinja2`` extension to be recognized as such. +The ``layout.jinja2`` template +------------------------------ + +Replace ``tutorial/templates/layout.jinja2`` with the following content: + +.. literalinclude:: src/views/tutorial/templates/layout.jinja2 + :linenos: + :emphasize-lines: 11,36 + :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 template inheritance via blocks. + +- We have defined 2 placeholders in the layout template where a child template + can override the content. These blocks are named ``title`` (line 11) and + ``content`` (line 36). +- Please refer to the Jinja2_ documentation for more information about + template inheritance. + The ``view.jinja2`` template ---------------------------- @@ -249,17 +269,21 @@ content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: - :emphasize-lines: 36,38-40 + :emphasize-lines: 1,4,6-8 :language: html This template is used by ``view_page()`` for displaying a single wiki page. It includes: +- We begin by extending the ``layout.jinja2`` template defined above + which provides the skeleton of the page (line 1). +- We override the ``content`` block from the base layout to insert our markup + into the body (line 3). - A variable that is replaced with the ``content`` value provided by the view - (line 36). ``content`` contains HTML, so the ``|safe`` filter is used to + (line 4). ``content`` contains HTML, so the ``|safe`` filter is used to prevent escaping it (e.g., changing ">" to ">"). - A link that points at the "edit" URL which invokes the ``edit_page`` view for - the page being viewed (lines 38-40). + the page being viewed (lines 6-8). The ``edit.jinja2`` template ---------------------------- @@ -269,18 +293,57 @@ content: .. literalinclude:: src/views/tutorial/templates/edit.jinja2 :linenos: - :emphasize-lines: 42,44,47 + :emphasize-lines: 3,12,14,17 :language: html This template is used by ``add_page()`` and ``edit_page()`` for adding and editing a wiki page. It displays a page containing a form that includes: +- Again we are extending the ``layout.jinja2`` template which provides + the skeleton of the page. +- Override the ``title`` block to affect the ```` tag in the + ``head`` of the page (line 3). - A 10-row by 60-column ``textarea`` field named ``body`` that is filled with - any existing page data when it is rendered (line 44). -- A submit button that has the name ``form.submitted`` (line 47). + any existing page data when it is rendered (line 14). +- A submit button that has the name ``form.submitted`` (line 17). The form POSTs back to the ``save_url`` argument supplied by the view (line -42). The view will use the ``body`` and ``form.submitted`` values. +12). The view will use the ``body`` and ``form.submitted`` values. + +The ``404.jinja2`` template +--------------------------- + +Replace ``tutorial/templates/404.jinja2`` with the following content: + +.. literalinclude:: src/views/tutorial/templates/404.jinja2 + :linenos: + :language: html + +This template is linked from the ``notfound_view`` defined in +``tutorial/views/notfound.py`` as shown here: + +.. literalinclude:: src/views/tutorial/views/notfound.py + :linenos: + :language: python + +There are several important things to note about this configuration: + +- The ``notfound_view`` in the above snippet is called an + :term:`exception view`. For more information see + :ref:`special_exceptions_in_callables`. +- The ``notfound_view`` sets the response status to 404. It's possible to + affect the response object used by the renderer via + :ref:`request_response_attr`. +- The ``notfound_view`` is registered as an exception view and will be invoked + **only** if ``pyramid.httpexceptions.HTTPNotFound`` is raised as an + exception. This means it will not be invoked for any responses returned + from a view normally. For example, on line 27 of + ``tutorial/views/default.py`` the exception is raised which will trigger + the view. + +Finally, you may delete the ``tutorial/templates/mytemplate.jinja2`` +template that was provided by the ``alchemy`` scaffold as we have created +our own templates for the wiki. .. note:: @@ -373,3 +436,5 @@ each of the following URLs, checking that the result is as expected: will generate a ``NoResultFound: No row was found for one()`` error. You'll see an interactive traceback facility provided by :term:`pyramid_debugtoolbar`. + +.. _jinja2: http://jinja.pocoo.org/ diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index 96df85a97..ca37f39f5 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -24,7 +24,7 @@ def view_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).first() if page is None: - return HTTPNotFound('No such page') + raise HTTPNotFound('No such page') def check(match): word = match.group(1) -- cgit v1.2.3 From 9a7cfe3b4e248451750f5694255450bf1983e848 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 10 Feb 2016 23:54:51 -0600 Subject: update 404 templates --- docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 | 2 +- docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 index 1917f83c7..37b0a16b6 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 @@ -2,7 +2,7 @@ {% block content %} <div class="content"> - <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1> <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> </div> {% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 index 1917f83c7..37b0a16b6 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 @@ -2,7 +2,7 @@ {% block content %} <div class="content"> - <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1> <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> </div> {% endblock content %} -- cgit v1.2.3 From f2e9c68e8168cfe51f7dc5ed86fea0471968f508 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 10 Feb 2016 23:55:03 -0600 Subject: move security into one place --- .../wiki2/src/authorization/tutorial/__init__.py | 11 +---- .../src/authorization/tutorial/models/mymodel.py | 14 ------ .../wiki2/src/authorization/tutorial/security.py | 51 ++++++++++++++++++++++ .../authorization/tutorial/security/__init__.py | 0 .../src/authorization/tutorial/security/default.py | 12 ----- 5 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security.py delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security/default.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index a62c42378..8eacdee5a 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -1,22 +1,13 @@ from pyramid.config import Configurator -from pyramid.authentication import AuthTktAuthenticationPolicy -from pyramid.authorization import ACLAuthorizationPolicy - -from .security.default import groupfinder def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - authn_policy = AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder, hashalg='sha512') - authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.set_root_factory('.models.mymodel.RootFactory') - config.set_authentication_policy(authn_policy) - config.set_authorization_policy(authz_policy) + config.include('.security') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py index 25209c745..b23d0c0d2 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py @@ -1,7 +1,3 @@ -from pyramid.security import ( - Allow, - Everyone, -) from sqlalchemy import ( Column, Integer, @@ -17,13 +13,3 @@ class Page(Base): id = Column(Integer, primary_key=True) name = Column(Text, unique=True) data = Column(Integer) - - -class RootFactory(object): - __acl__ = [ - (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit'), - ] - - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py new file mode 100644 index 000000000..7bceabf3f --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py @@ -0,0 +1,51 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +from pyramid.security import ( + Allow, + Authenticated, + Everyone, +) + + +USERS = { + 'editor': 'editor', + 'viewer': 'viewer', +} + +GROUPS = { + 'editor': ['group:editors'], +} + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + userid = self.unauthenticated_userid(request) + if userid in USERS: + return userid + + def effective_principals(self, request): + principals = [Everyone] + userid = self.authenticated_userid(request) + if userid is not None: + principals.append(Authenticated) + principals.append(userid) + + groups = GROUPS.get(userid, []) + principals.extend(groups) + return principals + +class RootFactory(object): + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit'), + ] + + def __init__(self, request): + pass + +def includeme(config): + authn_policy = MyAuthenticationPolicy('sosecret', hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_root_factory(RootFactory) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py deleted file mode 100644 index 7fc1ea7c8..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py +++ /dev/null @@ -1,12 +0,0 @@ -USERS = { - 'editor': 'editor', - 'viewer': 'viewer', -} - -GROUPS = { - 'editor': ['group:editors'], -} - -def groupfinder(userid, request): - if userid in USERS: - return GROUPS.get(userid, []) -- cgit v1.2.3 From cb5a84802171ed22b67958c7733cc0eddc680d34 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Thu, 11 Feb 2016 23:01:38 -0600 Subject: copy layout and templates from views to authorization --- .../authorization/tutorial/templates/edit.jinja2 | 93 +++++-------------- .../authorization/tutorial/templates/layout.jinja2 | 60 +++++++++++++ .../authorization/tutorial/templates/login.jinja2 | 100 ++++++--------------- .../authorization/tutorial/templates/view.jinja2 | 87 ++++-------------- .../src/authorization/tutorial/views/default.py | 2 +- .../src/views/tutorial/templates/layout.jinja2 | 5 ++ 6 files changed, 128 insertions(+), 219 deletions(-) create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 index 4d767cfbe..e47b3aabf 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -1,73 +1,20 @@ -<!DOCTYPE html> -<html lang="{{request.locale_name}}"> - <head> - <meta charset="utf-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <meta name="description" content="pyramid web application"> - <meta name="author" content="Pylons Project"> - <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> - - <title>Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} -

- Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

-
-
- -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block title %}Edit {{page.name}} - {% endblock title %} + +{% block content %} +

+Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the +FrontPage. +

+
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..82a144abf --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 @@ -0,0 +1,60 @@ + + + + + + + + + + + {% block title %}{% if page.name %} {{page.name}} - {% endif %}{% endblock title %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if request.authenticated_userid is not none %} +

+ Logout +

+ {% endif %} + {% block content %}{% endblock %} +
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 index a80a2a165..99d369173 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 @@ -1,74 +1,26 @@ - - - - - - - - - - - Login - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- - Login -
- {{ message }} -

-
- -
- - -
-
- - -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +

+ + Login +
+{{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 index 942b8479b..c582ce1f9 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -1,71 +1,16 @@ - - - - - - - - - - - {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} -

{{ content|safe }}

-

- - Edit this page - -

-

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block content %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the +FrontPage. +

+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index 6fb3c8744..e152e73e0 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -24,7 +24,7 @@ def view_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).first() if page is None: - return HTTPNotFound('No such page') + raise HTTPNotFound('No such page') def check(match): word = match.group(1) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 index 68743e0df..82a144abf 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -33,6 +33,11 @@
+ {% if request.authenticated_userid is not none %} +

+ Logout +

+ {% endif %} {% block content %}{% endblock %}
-- cgit v1.2.3 From 81e5989ed5b2bd7ea1a2b843dea9726b253b38ce Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 00:18:40 -0600 Subject: create an actual user model to prepare for security --- .../src/authorization/tutorial/views/default.py | 3 ++- docs/tutorials/wiki2/src/models/setup.py | 1 + .../wiki2/src/models/tutorial/models/__init__.py | 3 ++- .../wiki2/src/models/tutorial/models/mymodel.py | 15 ------------ .../wiki2/src/models/tutorial/models/page.py | 20 ++++++++++++++++ .../wiki2/src/models/tutorial/models/user.py | 28 ++++++++++++++++++++++ .../src/models/tutorial/scripts/initializedb.py | 16 +++++++++++-- 7 files changed, 67 insertions(+), 19 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/user.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index e152e73e0..f74059be0 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -10,6 +10,7 @@ from pyramid.view import view_config from ..models import Page + # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -42,7 +43,7 @@ def view_page(request): return dict(page=page, content=content, edit_url=edit_url) @view_config(route_name='add_page', renderer='../templates/edit.jinja2', - permission='edit') + permission='create') def add_page(request): pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index eb771010f..df9fec4d4 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -9,6 +9,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'bcrypt', 'pyramid', 'pyramid_jinja2', 'pyramid_debugtoolbar', diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py index 3d3efe06f..a8871f6f5 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -5,7 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import Page # flake8: noqa +from .page import Page # flake8: noqa +from .user import User # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py deleted file mode 100644 index b23d0c0d2..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, -) - -from .meta import Base - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Integer) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/page.py b/docs/tutorials/wiki2/src/models/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py new file mode 100644 index 000000000..6123a3aad --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -0,0 +1,28 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw, pre_hashed=False): + if pre_hashed: + pwhash = pw + else: + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py index 601a6e73f..175b7190f 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models import ( get_session_factory, get_tm_session, ) -from ..models import Page +from ..models import Page, User def usage(argv): @@ -41,5 +41,17 @@ def main(argv=sys.argv): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - page = Page(name='FrontPage', data='This is the front page') + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + viewer = User(name='viewer', role='viewer') + viewer.set_password('viewer') + dbsession.add(viewer) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) dbsession.add(page) -- cgit v1.2.3 From e6e4f655f2abe8d1d5ff63ecd70255094af6de73 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 01:09:01 -0600 Subject: let's go ahead and bite off more than we can chew by adding object-security we'll allow anyone to create pages, not just editors finally we'll allow page creators of pages to edit their pages even if they are not editors --- docs/tutorials/wiki2/definingmodels.rst | 65 +++++++++++------ docs/tutorials/wiki2/design.rst | 82 +++++++++++++--------- .../src/models/tutorial/scripts/initializedb.py | 6 +- 3 files changed, 94 insertions(+), 59 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index b90bf77e6..5af8110da 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -106,28 +106,49 @@ made to both the models.py file and to the initializedb.py file. See Success will look something like this:: - 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 - 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 - 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages") - 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] - CREATE TABLE pages ( - id INTEGER NOT NULL, - name TEXT, - data TEXT, - PRIMARY KEY (id), - UNIQUE (name) - ) - - - 2015-05-24 15:34:14,545 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-24 15:34:14,546 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - 2015-05-24 15:34:14,548 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) - 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data) VALUES (?, ?) - 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page') - 2015-05-24 15:34:14,550 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 + 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 + 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages") + 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("users") + 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-12 01:06:35,857 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE users ( + id INTEGER NOT NULL, + name TEXT NOT NULL, + role TEXT NOT NULL, + password_hash TEXT, + CONSTRAINT pk_users PRIMARY KEY (id), + CONSTRAINT uq_users_name UNIQUE (name) + ) + + + 2016-02-12 01:06:35,857 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-12 01:06:35,858 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-12 01:06:35,858 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE pages ( + id INTEGER NOT NULL, + name TEXT NOT NULL, + data INTEGER NOT NULL, + creator_id INTEGER NOT NULL, + CONSTRAINT pk_pages PRIMARY KEY (id), + CONSTRAINT uq_pages_name UNIQUE (name), + CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id) + ) + + + 2016-02-12 01:06:35,859 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-12 01:06:35,859 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-12 01:06:36,383 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) + 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?) + 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('editor', 'editor', '$2b$12$bSr5QR3wFs1LAnld7R94e.TXPj7DVoTxu2hA1kY6rm.Q3cAhD.AQO') + 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?) + 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('basic', 'basic', '$2b$12$.v0BQK2xWEQOnywbX2BFs.qzXo5Qf9oZohGWux/MOSj6Z.pVaY2Z6') + 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?) + 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page', 1) + 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT View the application in a browser --------------------------------- diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 8e3bb4c13..42f06f7bf 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -19,11 +19,17 @@ Models We'll be using an SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. -Within the database, we define a single table named `pages`, whose elements -will store the wiki pages. There are two columns: `name` and `data`. +Within the database, we will define two tables: -URLs like ``/PageName`` will try to find an element in the table that has a -corresponding name. +- The `users` table which will store the `name`, `password_hash` and `role`. +- The `pages` table, whose elements will store the wiki pages. + There are three columns: `name`, `data` and `creator_id`. + +There is a one-to-many relationship between `users` and `pages` tracking +the user who created each wiki page. + +URLs like ``/PageName`` will try to find an element in the `pages` table that +has a corresponding name. To add a page to the wiki, a new row is created and the text is stored in `data`. @@ -32,8 +38,8 @@ A page named ``FrontPage`` containing the text *This is the front page*, will be created when the storage is initialized, and will be used as the wiki home page. -Views ------ +Wiki Views +---------- There will be three views to handle the normal operations of adding, editing, and viewing wiki pages, plus one view for the wiki front page. Two templates @@ -47,33 +53,41 @@ templates. Security -------- -We'll eventually be adding security to our application. The components we'll -use to do this are below. - -- USERS, a dictionary mapping :term:`userids ` to their corresponding - passwords. - -- GROUPS, a dictionary mapping :term:`userids ` to a list of groups to - which they belong. - -- ``groupfinder``, an *authorization callback* that looks up USERS and GROUPS. - It will be provided in a new ``security/default.py`` subpackage and file. - -- An :term:`ACL` is attached to the root :term:`resource`. Each row below - details an :term:`ACE`: - - +----------+----------------+----------------+ - | Action | Principal | Permission | - +==========+================+================+ - | Allow | Everyone | View | - +----------+----------------+----------------+ - | Allow | group:editors | Edit | - +----------+----------------+----------------+ - -- Permission declarations are added to the views to assert the security - policies as each request is handled. - -Two additional views and one template will handle the login and logout tasks. +We'll eventually be adding security to our application. To do this, we'll +be using a very simple role-based security model. We'll assign a single +role category to each user in our system. + +`basic` + An authenticated user who can view content and create new pages. A `basic` + user may also edit the pages they have created but not pages created by + other users. + +`editor` + An authenticated user who can create and edit any content in the system. + +In order to accomplish this we'll need to define an authentication policy +which can identify users by their :term:`userid` and role. Then we'll +need to define a page :term:`resource` which contains the appropriate +:term:`ACL`: + ++----------+--------------------+----------------+ +| Action | Principal | Permission | ++==========+====================+================+ +| Allow | Everyone | view | ++----------+--------------------+----------------+ +| Allow | group:basic | create | ++----------+--------------------+----------------+ +| Allow | group:editors | edit | ++----------+--------------------+----------------+ +| Allow | | edit | ++----------+--------------------+----------------+ + +Permission declarations will be added to the views to assert the security +policies as each request is handled. + +On the security side of the application there are two additional views for +handling login and logout as well as two exception views for handling +invalid access attempts and unhandled URLs. Summary ------- @@ -102,7 +116,7 @@ in the following table: | | submitted, redirect | | | | | | to /PageName | | | | +----------------------+-----------------------+-------------+----------------+------------+ -| /add_page/PageName | Create the page | add_page | edit.jinja2 | edit | +| /add_page/PageName | Create the page | add_page | edit.jinja2 | create | | | *PageName* in | | | | | | storage, display | | | | | | the edit form | | | | diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py index 175b7190f..f3c0a6fef 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -45,9 +45,9 @@ def main(argv=sys.argv): editor.set_password('editor') dbsession.add(editor) - viewer = User(name='viewer', role='viewer') - viewer.set_password('viewer') - dbsession.add(viewer) + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) page = Page( name='FrontPage', -- cgit v1.2.3 From 574ba1aa6d81498220d123d149192eeba81afee7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 02:22:48 -0600 Subject: update the models chapter with the new user model --- docs/tutorials/wiki2/definingmodels.rst | 119 +++++++++++++-------- .../wiki2/src/models/tutorial/models/user.py | 11 +- 2 files changed, 82 insertions(+), 48 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 5af8110da..beb6cee5a 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -4,61 +4,84 @@ Defining the Domain Model The first change we'll make to our stock ``pcreate``-generated application will be to define a wiki page :term:`domain model`. -We'll do this inside our ``mymodel.py`` file. +.. note:: -Edit ``mymodel.py`` -------------------- + There is nothing special about the filename ``user.py`` or ``page.py`` except + that they are Python modules. A project may have many models throughout its + codebase in arbitrarily named modules. Modules implementing models often + have ``model`` in their names or they may live in a Python subpackage of + your application package named ``models`` (as we've done in this tutorial), + but this is only a convention and not a requirement. -.. note:: - There is nothing special about the filename ``mymodel.py`` except that it - is a Python module. A project may have many models throughout its codebase - in arbitrarily named modules. Modules implementing models often have - ``model`` in their names or they may live in a Python subpackage of your - application package named ``models`` (as we've done in this tutorial), but - this is only a convention and not a requirement. +Remove ``mymodel.py`` +--------------------- + +The first thing we'll do is delete the file ``tutorial/models/mymodel.py``. +The ``MyModel`` class is only a sample and we're not going to use it. -Open the ``tutorial/models/mymodel.py`` file and edit it to look like -the following: -.. literalinclude:: src/models/tutorial/models/mymodel.py +Add ``user.py`` +--------------- + +Create a new file ``tutorial/models/user.py`` with the following contents: + +.. literalinclude:: src/models/tutorial/models/user.py :linenos: :language: py - :emphasize-lines: 10-12,14-15 -The highlighted lines are the ones that need to be changed, as well as -removing lines that reference ``Index``. +This is a very basic model for a user who can authenticate with our wiki. + +We discussed briefly in the previous chapter that our models will inherit +from a SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This +will attach the model to our schema. + +As you can see, our ``User`` class has a class-level attribute +``__tablename__`` which equals the string ``users``. Our ``User`` class +will also have class-level attributes named ``id``, ``name``, +``password_hash`` and ``role`` (all instances of +:class:`sqlalchemy.schema.Column`). These will map to columns in the ``users`` +table. The ``id`` attribute will be the primary key in the table. The ``name`` +attribute will be a text column, each value of which needs to be unique within +the column. The ``password_hash`` is a nullable text attribute that will +contain a securely hashed password [1]_. Finally, the ``role`` text attribute +will hold the role of the user. -The first thing we've done is remove the stock ``MyModel`` class -from the generated ``models.py`` file. The ``MyModel`` class is only a -sample and we're not going to use it. +There are two helper methods that will help us later when using the +user objects. The first is ``set_password`` which will take a raw password +and transform it using bcrypt_ into an irreversible representation. The +``check_password`` method will allow us to compare input passwords to +see if they resolve to the same hash signifying a match. -Then we added a ``Page`` class. Because this is a SQLAlchemy application, -this class inherits from an instance of -:func:`sqlalchemy.ext.declarative.declarative_base`. -.. literalinclude:: src/models/tutorial/models/mymodel.py - :pyobject: Page +Add ``page.py`` +--------------- + +Create a new file ``tutorial/models/page.py`` with the following contents: + +.. literalinclude:: src/models/tutorial/models/page.py :linenos: - :language: python + :language: py -As you can see, our ``Page`` class has a class-level attribute -``__tablename__`` which equals the string ``'pages'``. This means that -SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our -``Page`` class will also have class-level attributes named ``id``, ``name``, -and ``data`` (all instances of :class:`sqlalchemy.schema.Column`). These will -map to columns in the ``pages`` table. The ``id`` attribute will be the -primary key in the table. The ``name`` attribute will be a text attribute, -each value of which needs to be unique within the column. The ``data`` -attribute is a text attribute that will hold the body of each page. +As you can see, our ``Page`` class is very similar to the ``User`` defined +above except with attributes focused on storing information about a wiki +page including ``id``, ``name``, and ``data``. The only new construct +introduced here is the ``creator_id`` column which is a foreign key +referencing the ``users`` table. Foreign keys are very useful at the +schema-level but since we want to relate ``User`` objects with ``Page`` +objects we also define a the ``creator`` attribute which is an ORM-level +mapping between the two tables. SQLAlchemy will automatically populate this +value using the foreign key referencing the user. Since the foreign key +has ``nullable=False`` we are guaranteed that an instance of ``page`` will +have a corresponding ``page.creator`` which will be a ``User`` instance. Edit ``models/__init__.py`` --------------------------- Since we are using a package for our models, we also need to update our -``__init__.py`` file to ensure that the model is attached to the metadata. +``__init__.py`` file to ensure that the models are attached to the metadata. Open the ``tutorial/models/__init__.py`` file and edit it to look like the following: @@ -66,9 +89,10 @@ the following: .. literalinclude:: src/models/tutorial/models/__init__.py :linenos: :language: py - :emphasize-lines: 8 + :emphasize-lines: 8,9 -Here we need to align our import with the name of the model ``Page``. +Here we need to align our imports with the names of the models ``User``, +and ``Page``. Edit ``scripts/initializedb.py`` @@ -77,13 +101,13 @@ Edit ``scripts/initializedb.py`` We haven't looked at the details of this file yet, but within the ``scripts`` directory of your ``tutorial`` package is a file named ``initializedb.py``. Code in this file is executed whenever we run the ``initialize_tutorial_db`` -command, as we did in the installation step of this tutorial. +command, as we did in the installation step of this tutorial [2]_. Since we've changed our model, we need to make changes to our ``initializedb.py`` script. In particular, we'll replace our import of -``MyModel`` with one of ``Page`` and we'll change the very end of the script -to create a ``Page`` rather than a ``MyModel`` and add it to our -``dbsession``. +``MyModel`` with those of ``User`` and ``Page`` and we'll change the very end +of the script to create two ``User`` objects (``basic`` and ``editor``) and a +``Page`` rather than a ``MyModel`` and add them to our ``dbsession``. Open ``tutorial/scripts/initializedb.py`` and edit it to look like the following: @@ -91,7 +115,7 @@ the following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: :language: python - :emphasize-lines: 18,44-45 + :emphasize-lines: 18,44-57 Only the highlighted lines need to be changed. @@ -164,3 +188,14 @@ up with a Python traceback on your console that ends with this exception: ImportError: cannot import name MyModel This will also happen if you attempt to run the tests. + +.. _bcrypt: https://pypi.python.org/pypi/bcrypt + +.. [1] We are using the bcrypt_ package from PyPI to hash our passwords + securely. There are other one-way hash algorithms for passwords if + bcrypt is an issue on your system. Just make sure that it's an + algorithm approved for storing passwords versus a generic one-way hash. + +.. [2] The command is named ``initialize_tutorial_db`` because of the mapping + defined in the ``[console_scripts]`` entry point of our project's + ``setup.py`` file. diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py index 6123a3aad..25b0a8187 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -17,12 +17,11 @@ class User(Base): password_hash = Column(Text) - def set_password(self, pw, pre_hashed=False): - if pre_hashed: - pwhash = pw - else: - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + if self.password_hash is not None: + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + return False -- cgit v1.2.3 From a115c6d30fe8e497f67604370db4ffc8f2b124a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 02:42:04 -0600 Subject: add the bcrypt dependency --- docs/tutorials/wiki2/definingmodels.rst | 22 ++++++++++++++++++++++ docs/tutorials/wiki2/design.rst | 8 +++++--- 2 files changed, 27 insertions(+), 3 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index beb6cee5a..33e7beb4f 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -15,6 +15,28 @@ be to define a wiki page :term:`domain model`. but this is only a convention and not a requirement. +Declaring dependencies in our ``setup.py`` file +=============================================== + +The models code in our application will depend on a package which is not a +dependency of the original "tutorial" application. The original "tutorial" +application was generated by the ``pcreate`` command; it doesn't know +about our custom application requirements. + +We need to add a dependency on the ``bcrypt`` package to our ``tutorial`` +package's ``setup.py`` file by assigning this dependency to the ``requires`` +parameter in the ``setup()`` function. + +Open ``tutorial/setup.py`` and edit it to look like the following: + +.. literalinclude:: src/models/setup.py + :linenos: + :emphasize-lines: 12 + :language: python + +Only the highlighted line needs to be added. + + Remove ``mymodel.py`` --------------------- diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 42f06f7bf..de43447d3 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -21,12 +21,14 @@ We'll be using an SQLite database to hold our wiki data, and we'll be using Within the database, we will define two tables: -- The `users` table which will store the `name`, `password_hash` and `role`. +- The `users` table which will store the `id`, `name`, `password_hash` and + `role` of each wiki user. - The `pages` table, whose elements will store the wiki pages. - There are three columns: `name`, `data` and `creator_id`. + There are four columns: `id`, `name`, `data` and `creator_id`. There is a one-to-many relationship between `users` and `pages` tracking -the user who created each wiki page. +the user who created each wiki page defined by the `creator_id` column on the +`pages` table. URLs like ``/PageName`` will try to find an element in the `pages` table that has a corresponding name. -- cgit v1.2.3 From 4872a1e713f894b383990f62cf82c2b21f810c16 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 02:48:09 -0600 Subject: forward port changes to models / scripts to later chapters --- docs/tutorials/wiki2/src/authorization/setup.py | 3 ++- .../src/authorization/tutorial/models/__init__.py | 3 ++- .../src/authorization/tutorial/models/mymodel.py | 15 ----------- .../src/authorization/tutorial/models/page.py | 20 +++++++++++++++ .../src/authorization/tutorial/models/user.py | 27 ++++++++++++++++++++ .../authorization/tutorial/scripts/initializedb.py | 16 ++++++++++-- docs/tutorials/wiki2/src/tests/setup.py | 3 ++- .../wiki2/src/tests/tutorial/models/__init__.py | 3 ++- .../wiki2/src/tests/tutorial/models/mymodel.py | 29 ---------------------- .../wiki2/src/tests/tutorial/models/page.py | 20 +++++++++++++++ .../wiki2/src/tests/tutorial/models/user.py | 27 ++++++++++++++++++++ .../src/tests/tutorial/scripts/initializedb.py | 16 ++++++++++-- docs/tutorials/wiki2/src/views/setup.py | 3 ++- .../wiki2/src/views/tutorial/models/__init__.py | 3 ++- .../wiki2/src/views/tutorial/models/mymodel.py | 15 ----------- .../wiki2/src/views/tutorial/models/page.py | 20 +++++++++++++++ .../wiki2/src/views/tutorial/models/user.py | 27 ++++++++++++++++++++ .../src/views/tutorial/scripts/initializedb.py | 16 ++++++++++-- 18 files changed, 195 insertions(+), 71 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/user.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/user.py delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/user.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index d4e5a4072..c342c1aba 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'bcrypt', + 'docutils', 'pyramid', 'pyramid_jinja2', 'pyramid_debugtoolbar', @@ -17,7 +19,6 @@ requires = [ 'transaction', 'zope.sqlalchemy', 'waitress', - 'docutils', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py index 3d3efe06f..a8871f6f5 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -5,7 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import Page # flake8: noqa +from .page import Page # flake8: noqa +from .user import User # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py deleted file mode 100644 index b23d0c0d2..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, -) - -from .meta import Base - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Integer) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py new file mode 100644 index 000000000..25b0a8187 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py @@ -0,0 +1,27 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + return False diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py index 601a6e73f..f3c0a6fef 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models import ( get_session_factory, get_tm_session, ) -from ..models import Page +from ..models import Page, User def usage(argv): @@ -41,5 +41,17 @@ def main(argv=sys.argv): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - page = Page(name='FrontPage', data='This is the front page') + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index 93195a68c..e06aa06e4 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'bcrypt', + 'docutils', 'pyramid', 'pyramid_jinja2', 'pyramid_debugtoolbar', @@ -17,7 +19,6 @@ requires = [ 'transaction', 'zope.sqlalchemy', 'waitress', - 'docutils', ] tests_require = [ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py index 3d3efe06f..a8871f6f5 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py @@ -5,7 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import Page # flake8: noqa +from .page import Page # flake8: noqa +from .user import User # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py deleted file mode 100644 index 25209c745..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py +++ /dev/null @@ -1,29 +0,0 @@ -from pyramid.security import ( - Allow, - Everyone, -) -from sqlalchemy import ( - Column, - Integer, - Text, -) - -from .meta import Base - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Integer) - - -class RootFactory(object): - __acl__ = [ - (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit'), - ] - - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/page.py b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py new file mode 100644 index 000000000..25b0a8187 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py @@ -0,0 +1,27 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + return False diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py index 601a6e73f..f3c0a6fef 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models import ( get_session_factory, get_tm_session, ) -from ..models import Page +from ..models import Page, User def usage(argv): @@ -41,5 +41,17 @@ def main(argv=sys.argv): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - page = Page(name='FrontPage', data='This is the front page') + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index d4e5a4072..c342c1aba 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'bcrypt', + 'docutils', 'pyramid', 'pyramid_jinja2', 'pyramid_debugtoolbar', @@ -17,7 +19,6 @@ requires = [ 'transaction', 'zope.sqlalchemy', 'waitress', - 'docutils', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py index 3d3efe06f..a8871f6f5 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -5,7 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import Page # flake8: noqa +from .page import Page # flake8: noqa +from .user import User # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py deleted file mode 100644 index b23d0c0d2..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, -) - -from .meta import Base - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Integer) diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/page.py b/docs/tutorials/wiki2/src/views/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py new file mode 100644 index 000000000..25b0a8187 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py @@ -0,0 +1,27 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + return False diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py index 601a6e73f..f3c0a6fef 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models import ( get_session_factory, get_tm_session, ) -from ..models import Page +from ..models import Page, User def usage(argv): @@ -41,5 +41,17 @@ def main(argv=sys.argv): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - page = Page(name='FrontPage', data='This is the front page') + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) dbsession.add(page) -- cgit v1.2.3 From 23c2d7b337a5873dba0ca6c146e1174136ac2187 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 02:54:37 -0600 Subject: update the views/models with setup.py develop --- docs/tutorials/wiki2/definingmodels.rst | 30 +++++++++++++++++++++++ docs/tutorials/wiki2/definingviews.rst | 43 ++++++--------------------------- docs/tutorials/wiki2/design.rst | 10 ++++---- 3 files changed, 43 insertions(+), 40 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 33e7beb4f..41f36fa26 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -37,6 +37,36 @@ Open ``tutorial/setup.py`` and edit it to look like the following: Only the highlighted line needs to be added. +Running ``setup.py develop`` +============================ + +Since a new software dependency was added, you will need to run ``python +setup.py develop`` again inside the root of the ``tutorial`` package to obtain +and register the newly added dependency distribution. + +Make sure your current working directory is the root of the project (the +directory in which ``setup.py`` lives) and execute the following command. + +On UNIX: + +.. code-block:: bash + + $ cd tutorial + $ $VENV/bin/python setup.py develop + +On Windows: + +.. code-block:: text + + c:\pyramidtut> cd tutorial + c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop + +Success executing this command will end with a line to the console something +like this:: + + Finished processing dependencies for tutorial==0.0 + + Remove ``mymodel.py`` --------------------- diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 6629839f8..8bccc3fc0 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -14,13 +14,12 @@ and a user visits ``http://example.com/foo/bar``, our pattern would be matched against ``/foo/bar`` and the ``matchdict`` would look like ``{'one':'foo', 'two':'bar'}``. -Declaring dependencies in our ``setup.py`` file -=============================================== +Adding the ``docutils`` dependency +================================== -The view code in our application will depend on a package which is not a -dependency of the original "tutorial" application. The original "tutorial" -application was generated by the ``pcreate`` command; it doesn't know -about our custom application requirements. +Remember in the previous chapter we added a new dependency on the ``bcrypt`` +package. Again, the view code in our application will depend on a package which +is not a dependency of the original "tutorial" application. We need to add a dependency on the ``docutils`` package to our ``tutorial`` package's ``setup.py`` file by assigning this dependency to the ``requires`` @@ -30,39 +29,13 @@ Open ``tutorial/setup.py`` and edit it to look like the following: .. literalinclude:: src/views/setup.py :linenos: - :emphasize-lines: 20 + :emphasize-lines: 13 :language: python Only the highlighted line needs to be added. -Running ``setup.py develop`` -============================ - -Since a new software dependency was added, you will need to run ``python -setup.py develop`` again inside the root of the ``tutorial`` package to obtain -and register the newly added dependency distribution. - -Make sure your current working directory is the root of the project (the -directory in which ``setup.py`` lives) and execute the following command. - -On UNIX: - -.. code-block:: bash - - $ cd tutorial - $ $VENV/bin/python setup.py develop - -On Windows: - -.. code-block:: text - - c:\pyramidtut> cd tutorial - c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop - -Success executing this command will end with a line to the console something -like this:: - - Finished processing dependencies for tutorial==0.0 +Again, as we did in the previous chapter, the dependency now needs to be +installed so re-run the ``python setup.py develop`` command. Adding view functions in ``views/default.py`` ============================================= diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index de43447d3..45e2fddd0 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -6,7 +6,7 @@ Following is a quick overview of the design of our wiki application to help us understand the changes that we will be making as we work through the tutorial. Overall -------- +======= We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` @@ -14,7 +14,7 @@ Python module. We will add this module in the dependency list on the project ``setup.py`` file. Models ------- +====== We'll be using an SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. @@ -41,7 +41,7 @@ be created when the storage is initialized, and will be used as the wiki home page. Wiki Views ----------- +========== There will be three views to handle the normal operations of adding, editing, and viewing wiki pages, plus one view for the wiki front page. Two templates @@ -53,7 +53,7 @@ designer-friendly templating language for Python, modeled after Django's templates. Security --------- +======== We'll eventually be adding security to our application. To do this, we'll be using a very simple role-based security model. We'll assign a single @@ -92,7 +92,7 @@ handling login and logout as well as two exception views for handling invalid access attempts and unhandled URLs. Summary -------- +======= The URL, actions, template, and permission associated to each view are listed in the following table: -- cgit v1.2.3 From 60891b844c883d2c9ce864522f2202d9514d8d83 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 13 Feb 2016 13:26:05 -0600 Subject: improve the views section by removing quirks and explaining transactions --- docs/tutorials/wiki2/definingviews.rst | 274 ++++++++++++--------- .../wiki2/src/views/tutorial/templates/edit.jinja2 | 6 +- .../src/views/tutorial/templates/layout.jinja2 | 7 +- .../wiki2/src/views/tutorial/templates/view.jinja2 | 4 +- .../wiki2/src/views/tutorial/views/default.py | 36 +-- 5 files changed, 185 insertions(+), 142 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 8bccc3fc0..8f0f7b51d 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -37,6 +37,80 @@ Only the highlighted line needs to be added. Again, as we did in the previous chapter, the dependency now needs to be installed so re-run the ``python setup.py develop`` command. +Static assets +------------- + +Our templates name static assets, including CSS and images. We don't need +to create these files within our package's ``static`` directory because they +were provided at the time we created the project. + +As an example, the CSS file will be accessed via +``http://localhost:6543/static/theme.css`` by virtue of the call to the +``add_static_view`` directive we've made in the ``__init__.py`` file. Any +number and type of static assets can be placed in this directory (or +subdirectories) and are just referred to by URL or by using the convenience +method ``static_url``, e.g., +``request.static_url(':static/foo.css')`` within templates. + +Adding routes to ``__init__.py`` +================================ + +This is the URL Dispatch tutorial and so let's start by adding some +URL patterns to our app. Later we'll attach views to handle the URLs. + +The ``__init__.py`` file contains +:meth:`pyramid.config.Configurator.add_route` calls which serve to add routes +to our application. First, we’ll get rid of the existing route created by +the template using the name ``'home'``. It’s only an example and isn’t +relevant to our application. + +We then need to add four calls to ``add_route``. Note that the *ordering* of +these declarations is very important. ``route`` declarations are matched in +the order they're found in the ``__init__.py`` file. + +#. Add a declaration which maps the pattern ``/`` (signifying the root URL) + to the route named ``view_wiki``. It maps to our ``view_wiki`` view + callable by virtue of the ``@view_config`` attached to the ``view_wiki`` + view function indicating ``route_name='view_wiki'``. + +#. Add a declaration which maps the pattern ``/{pagename}`` to the route named + ``view_page``. This is the regular view for a page. It maps + to our ``view_page`` view callable by virtue of the ``@view_config`` + attached to the ``view_page`` view function indicating + ``route_name='view_page'``. + +#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the + route named ``add_page``. This is the add view for a new page. It maps + to our ``add_page`` view callable by virtue of the ``@view_config`` + attached to the ``add_page`` view function indicating + ``route_name='add_page'``. + +#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the + route named ``edit_page``. This is the edit view for a page. It maps + to our ``edit_page`` view callable by virtue of the ``@view_config`` + attached to the ``edit_page`` view function indicating + ``route_name='edit_page'``. + +As a result of our edits, the ``__init__.py`` file should look +something like: + +.. literalinclude:: src/views/tutorial/__init__.py + :linenos: + :emphasize-lines: 11-14 + :language: python + +The highlighted lines are the ones that need to be added or edited. + +.. warn:: + + The order of the routes is important! If you placed + ``/{pagename}/edit_page`` **before** ``/add_page/{pagename}`` then we would + never be able to add pages because the first route would always match + a request to ``/add_page/edit_page`` whereas we want ``/add_page/..`` to + have priority. This isn't a huge problem in this particular app because + wiki pages are always camel case but it's important to be aware of this + behavior in your own apps. + Adding view functions in ``views/default.py`` ============================================= @@ -46,7 +120,7 @@ edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py :linenos: :language: python - :emphasize-lines: 1-9,12-68 + :emphasize-lines: 1-9,12-70 The highlighted lines need to be added or edited. @@ -54,7 +128,7 @@ We added some imports, and created a regular expression to find "WikiWords". We got rid of the ``my_view`` view function and its decorator that was added when we originally rendered the ``alchemy`` scaffold. It was only an example -and isn't relevant to our application. We also delated the ``db_err_msg`` +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`` @@ -88,7 +162,7 @@ Following is the code for the ``view_wiki`` view function and its decorator: :language: python ``view_wiki()`` is the :term:`default view` that gets called when a request is -made to the root URL of our wiki. It always redirects to an URL which +made to the root URL of our wiki. It always redirects to a URL which represents the path to our "FrontPage". The ``view_wiki`` view callable always redirects to the URL of a Page resource @@ -96,7 +170,7 @@ named "FrontPage". To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement the :class:`pyramid.interfaces.IResponse` interface, like :class:`pyramid.response.Response` does). It uses the -:meth:`pyramid.request.Request.route_url` API to construct an URL to the +: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. @@ -116,12 +190,12 @@ Here is the code for the ``view_page`` view function and its decorator: ``Page`` model object) as HTML. Then it substitutes an HTML anchor for each *WikiWord* reference in the rendered HTML using a compiled regular expression. -The curried function named ``check`` is used as the first argument to +The curried function named ``add_link`` is used as the first argument to ``wikiwords.sub``, indicating that it should be called to provide a value for each WikiWord match found in the content. If the wiki already contains a -page with the matched WikiWord name, ``check()`` generates a view +page with the matched WikiWord name, ``add_link()`` generates a view link to be used as the substitution value and returns it. If the wiki does -not already contain a page with the matched WikiWord name, ``check()`` +not already contain a page with the matched WikiWord name, ``add_link()`` generates an "add" link as the substitution value and returns it. As a result, the ``content`` variable is now a fully formed bit of HTML @@ -136,19 +210,73 @@ associated with the view configuration to render a response. In our case, the renderer used will be the ``view.jinja2`` template, as indicated in the ``@view_config`` decorator that is applied to ``view_page()``. +If the page does not exist then we need to handle that by raising +:class:`pyramid.httpexceptions.HTTPNotFound`` to trigger our 404 handling +defined in ``tutorial/views/notfound.py``. + +.. note:: + + Using ``raise`` versus ``return`` with the http exceptions is an important + distinction that can commonly mess people up. In + ``tutorial/views/notfound.py`` there is an :term:`exception view` + registered for handling the ``HTTPNotFound`` exception. Exception views + are only triggered for raised exceptions. If the ``HTTPNotFound`` is + returned then it has an internal "stock" template that it will use + to render itself as a response. If you aren't seeing your exception + view being executed this is probably the problem! See + :ref:`special_exceptions_in_callables` for more information about + exception views. + +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 + +``edit_page()`` is invoked when a user clicks the "Edit this +Page" button on the view form. It renders an edit form, but it also acts as +the handler for the form it renders. The ``matchdict`` attribute of the +request passed to the ``edit_page`` view will have a ``'pagename'`` key +matching the name of the page the user wants to edit. + +If the view execution *is* a result of a form submission (i.e., the expression +``'form.submitted' in request.params`` is ``True``), the view grabs the +``body`` element of the request parameters and sets it as the ``data`` +attribute of the page object. It then redirects to the ``view_page`` view +of the wiki page. + +If the view execution is *not* a result of a form submission (i.e., the +expression ``'form.submitted' in request.params`` is ``False``), the view +simply renders the edit form, passing the page object and a ``save_url`` +which will be used as the action of the generated form. + +.. note:: + + Since our ``request.dbsession`` defined in the previous chapter is + registered with the ``pyramid_tm`` transaction manager any changes we make + to objects managed by the that session will be committed automatically. + In the event that there was an error (even later, in our template code) the + changes would be aborted. This means the view itself does not need to + concern itself with commit/rollback logic. + The ``add_page`` view function ------------------------------ Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 44-55 + :lines: 58-70 :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 ``check`` function +isn't yet represented as a page in the system. The ``add_link`` function within the ``view_page`` view generates URLs to this view. ``add_page()`` also acts as a handler for the form that is generated when we want to add a page object. The ``matchdict`` attribute of the @@ -164,8 +292,12 @@ If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), we grab the page body from the form data, create a Page object with this page body and the name taken from ``matchdict['pagename']``, and save it into the database using -``request.dbession.add``. We then redirect back to the ``view_page`` view for -the newly created page. +``request.dbession.add``. Since we have not yet covered authentication we +don't have a logged-in user to add as the page's ``creator``. Until we +get to that point in the tutorial we'll just assume that all pages are created +by the ``editor`` user so we query that object and set it on ``page.creator``. +Finally, we redirect the client back to the ``view_page`` view for the newly +created page. If the view execution is *not* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``False``), the view @@ -177,34 +309,6 @@ in order to satisfy the edit form's desire to have *some* page object exposed as ``page``. :app:`Pyramid` will render the template associated with this view to a response. -The ``edit_page`` view function -------------------------------- - -Here is the code for the ``edit_page`` view function and its decorator: - -.. literalinclude:: src/views/tutorial/views/default.py - :lines: 57-68 - :lineno-match: - :linenos: - :language: python - -``edit_page()`` is invoked when a user clicks the "Edit this -Page" button on the view form. It renders an edit form, but it also acts as -the handler for the form it renders. The ``matchdict`` attribute of the -request passed to the ``edit_page`` view will have a ``'pagename'`` key -matching the name of the page the user wants to edit. - -If the view execution *is* a result of a form submission (i.e., the expression -``'form.submitted' in request.params`` is ``True``), the view grabs the -``body`` element of the request parameters and sets it as the ``data`` -attribute of the page object. It then redirects to the ``view_page`` view -of the wiki page. - -If the view execution is *not* a result of a form submission (i.e., the -expression ``'form.submitted' in request.params`` is ``False``), the view -simply renders the edit form, passing the page object and a ``save_url`` -which will be used as the action of the generated form. - Adding templates ================ @@ -229,7 +333,7 @@ our page templates into reusable components. One method for doing this is template inheritance via blocks. - We have defined 2 placeholders in the layout template where a child template - can override the content. These blocks are named ``title`` (line 11) and + 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. @@ -237,44 +341,45 @@ is template inheritance via blocks. The ``view.jinja2`` template ---------------------------- -Create ``tutorial/templates/view.jinja2`` and add the following -content: +Create ``tutorial/templates/view.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: :emphasize-lines: 1,4,6-8 :language: html -This template is used by ``view_page()`` for displaying a single -wiki page. It includes: +This template is used by ``view_page()`` for displaying a single wiki page. +It includes: - We begin by extending the ``layout.jinja2`` template defined above which provides the skeleton of the page (line 1). +- We override the ``subtitle`` block from the base layout to insert the + page name of the page into the page's title (line 3). - We override the ``content`` block from the base layout to insert our markup - into the body (line 3). + into the body (line 5-18). - A variable that is replaced with the ``content`` value provided by the view - (line 4). ``content`` contains HTML, so the ``|safe`` filter is used to + (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to prevent escaping it (e.g., changing ">" to ">"). - A link that points at the "edit" URL which invokes the ``edit_page`` view for - the page being viewed (lines 6-8). + the page being viewed (line 9). The ``edit.jinja2`` template ---------------------------- -Create ``tutorial/templates/edit.jinja2`` and add the following -content: +Create ``tutorial/templates/edit.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/edit.jinja2 :linenos: :emphasize-lines: 3,12,14,17 :language: html -This template is used by ``add_page()`` and ``edit_page()`` for adding and -editing a wiki page. It displays a page containing a form that includes: +This template serves two use-cases. It is used by ``add_page()`` and +``edit_page()`` for adding and editing a wiki page. It displays a page +containing a form that includes: -- Again we are extending the ``layout.jinja2`` template which provides - the skeleton of the page. -- Override the ``title`` block to affect the ```` tag in the +- Again, we are extending the ``layout.jinja2`` template which provides + the skeleton of the page (line 1). +- Override the ``subtitle`` block to affect the ``<title>`` tag in the ``head`` of the page (line 3). - A 10-row by 60-column ``textarea`` field named ``body`` that is filled with any existing page data when it is rendered (line 14). @@ -326,67 +431,6 @@ our own templates for the wiki. See :ref:`renderer_system_values` for information about other names that are available by default when a template is used as a renderer. -Static assets -------------- - -Our templates name static assets, including CSS and images. We don't need -to create these files within our package's ``static`` directory because they -were provided at the time we created the project. - -As an example, the CSS file will be accessed via -``http://localhost:6543/static/theme.css`` by virtue of the call to the -``add_static_view`` directive we've made in the ``__init__.py`` file. Any -number and type of static assets can be placed in this directory (or -subdirectories) and are just referred to by URL or by using the convenience -method ``static_url``, e.g., -``request.static_url('<package>:static/foo.css')`` within templates. - -Adding routes to ``__init__.py`` -================================ - -The ``__init__.py`` file contains -:meth:`pyramid.config.Configurator.add_route` calls which serve to add routes -to our application. First, we’ll get rid of the existing route created by -the template using the name ``'home'``. It’s only an example and isn’t -relevant to our application. - -We then need to add four calls to ``add_route``. Note that the *ordering* of -these declarations is very important. ``route`` declarations are matched in -the order they're found in the ``__init__.py`` file. - -#. Add a declaration which maps the pattern ``/`` (signifying the root URL) - to the route named ``view_wiki``. It maps to our ``view_wiki`` view - callable by virtue of the ``@view_config`` attached to the ``view_wiki`` - view function indicating ``route_name='view_wiki'``. - -#. Add a declaration which maps the pattern ``/{pagename}`` to the route named - ``view_page``. This is the regular view for a page. It maps - to our ``view_page`` view callable by virtue of the ``@view_config`` - attached to the ``view_page`` view function indicating - ``route_name='view_page'``. - -#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the - route named ``add_page``. This is the add view for a new page. It maps - to our ``add_page`` view callable by virtue of the ``@view_config`` - attached to the ``add_page`` view function indicating - ``route_name='add_page'``. - -#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the - route named ``edit_page``. This is the edit view for a page. It maps - to our ``edit_page`` view callable by virtue of the ``@view_config`` - attached to the ``edit_page`` view function indicating - ``route_name='edit_page'``. - -As a result of our edits, the ``__init__.py`` file should look -something like: - -.. literalinclude:: src/views/tutorial/__init__.py - :linenos: - :emphasize-lines: 11-14 - :language: python - -The highlighted lines are the ones that need to be added or edited. - Viewing the application in a browser ==================================== diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 index e47b3aabf..7db25c674 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -1,17 +1,17 @@ {% extends 'layout.jinja2' %} -{% block title %}Edit {{page.name}} - {% endblock title %} +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} {% block content %} <p> -Editing <strong>{% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}</strong> +Editing <strong>{{pagename}}</strong> </p> <p>You can return to the <a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. </p> <form action="{{ save_url }}" method="post"> <div class="form-group"> - <textarea class="form-control" name="body" rows="10" cols="60">{{ page.data }}</textarea> + <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea> </div> <div class="form-group"> <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 index 82a144abf..71785157f 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -8,7 +8,7 @@ <meta name="author" content="Pylons Project"> <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> - <title>{% block title %}{% if page.name %} {{page.name}} - {% endif %}{% endblock title %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + {% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) @@ -33,11 +33,6 @@
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} {% block content %}{% endblock %}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 index c582ce1f9..94419e228 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -1,5 +1,7 @@ {% extends 'layout.jinja2' %} +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + {% block content %}

{{ content|safe }}

@@ -8,7 +10,7 @@

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} + Viewing {{page.name}}, created by {{page.creator.name}}.

You can return to the FrontPage. diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index ca37f39f5..7a4073b3f 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -9,7 +9,7 @@ from pyramid.httpexceptions import ( from pyramid.view import view_config -from ..models import Page +from ..models import Page, User # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -26,7 +26,7 @@ def view_page(request): if page is None: raise HTTPNotFound('No such page') - def check(match): + def add_link(match): word = match.group(1) exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: @@ -37,23 +37,10 @@ def view_page(request): return '%s' % (add_url, cgi.escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) + content = wikiwords.sub(add_link, content) edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='../templates/edit.jinja2') -def add_page(request): - pagename = request.matchdict['pagename'] - if 'form.submitted' in request.params: - body = request.params['body'] - page = Page(name=pagename, data=body) - request.dbsession.add(page) - next_url = request.route_url('view_page', pagename=pagename) - return HTTPFound(location=next_url) - save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url) - @view_config(route_name='edit_page', renderer='../templates/edit.jinja2') def edit_page(request): pagename = request.matchdict['pagename'] @@ -63,6 +50,21 @@ def edit_page(request): next_url = request.route_url('view_page', pagename=pagename) return HTTPFound(location=next_url) return dict( - page=page, + pagename=page.name, + pagedata=page.data, save_url=request.route_url('edit_page', pagename=pagename), ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2') +def add_page(request): + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + page.creator = ( + request.dbsession.query(User).filter_by(name='editor').one()) + request.dbsession.add(page) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + save_url = request.route_url('add_page', pagename=pagename) + return dict(pagename=pagename, pagedata='', save_url=save_url) -- cgit v1.2.3 From 4c391c55057acbb36df28215f562c42d2b616872 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 13 Feb 2016 21:01:09 -0600 Subject: fix syntax --- 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 8f0f7b51d..acaefe6e8 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -101,7 +101,7 @@ something like: The highlighted lines are the ones that need to be added or edited. -.. warn:: +.. warning:: The order of the routes is important! If you placed ``/{pagename}/edit_page`` **before** ``/add_page/{pagename}`` then we would -- cgit v1.2.3 From bca6c996d9e879c21d8b207bb36bc10ebe1db256 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 13 Feb 2016 21:08:58 -0600 Subject: highlight more appropriate lines in views --- docs/tutorials/wiki2/definingviews.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index acaefe6e8..fea682628 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -345,7 +345,7 @@ Create ``tutorial/templates/view.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: - :emphasize-lines: 1,4,6-8 + :emphasize-lines: 1,3,6 :language: html This template is used by ``view_page()`` for displaying a single wiki page. @@ -402,6 +402,7 @@ This template is linked from the ``notfound_view`` defined in .. literalinclude:: src/views/tutorial/views/notfound.py :linenos: + :emphasize-lines: 6 :language: python There are several important things to note about this configuration: @@ -409,8 +410,8 @@ There are several important things to note about this configuration: - The ``notfound_view`` in the above snippet is called an :term:`exception view`. For more information see :ref:`special_exceptions_in_callables`. -- The ``notfound_view`` sets the response status to 404. It's possible to - affect the response object used by the renderer via +- The ``notfound_view`` sets the response status to 404. It's possible + to affect the response object used by the renderer via :ref:`request_response_attr`. - The ``notfound_view`` is registered as an exception view and will be invoked **only** if ``pyramid.httpexceptions.HTTPNotFound`` is raised as an -- cgit v1.2.3 From 42c93166fbe676341b1d94ec3659ae772dd073d8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 14 Feb 2016 17:35:01 -0600 Subject: fix unicode issues with check_password --- docs/tutorials/wiki2/src/models/tutorial/models/user.py | 6 ++++-- docs/tutorials/wiki2/src/views/tutorial/models/user.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py index 25b0a8187..6fb32a1b2 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -18,10 +18,12 @@ class User(Base): password_hash = Column(Text) def set_password(self, pw): - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): if self.password_hash is not None: - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py index 25b0a8187..6fb32a1b2 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py @@ -18,10 +18,12 @@ class User(Base): password_hash = Column(Text) def set_password(self, pw): - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): if self.password_hash is not None: - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash return False -- cgit v1.2.3 From da5ebc28c38ea32ad99389b5bc23e2f847af8047 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 14 Feb 2016 17:40:03 -0600 Subject: split routes into a separate module --- docs/tutorials/wiki2/basiclayout.rst | 64 +++++++++++++--------- docs/tutorials/wiki2/definingviews.rst | 17 +++--- .../wiki2/src/basiclayout/tutorial/__init__.py | 3 +- .../wiki2/src/basiclayout/tutorial/routes.py | 3 + .../wiki2/src/models/tutorial/__init__.py | 3 +- docs/tutorials/wiki2/src/models/tutorial/routes.py | 3 + .../tutorials/wiki2/src/views/tutorial/__init__.py | 6 +- docs/tutorials/wiki2/src/views/tutorial/routes.py | 6 ++ 8 files changed, 62 insertions(+), 43 deletions(-) create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/routes.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 485d38047..1ae51eb93 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -74,35 +74,19 @@ exact setup of the models will be covered later. :lineno-match: :language: py -``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with -two arguments: ``static`` (the name), and ``static`` (the path): +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: 10 :lineno-match: :language: py -This registers a static resource view which will match any URL that starts -with the prefix ``/static`` (by virtue of the first argument to -``add_static_view``). This will serve up static resources for us from within -the ``static`` directory of our ``tutorial`` package, in this case, via -``http://localhost:6543/static/`` and below (by virtue of the second argument -to ``add_static_view``). With this declaration, we're saying that any URL that -starts with ``/static`` should go to the static view; any remainder of its -path (e.g. the ``/foo`` in ``/static/foo``) will be used to compose a path to -a static file resource, such as a CSS file. - -Using the configurator ``main`` also registers a :term:`route configuration` -via the :meth:`pyramid.config.Configurator.add_route` method that will be -used when the URL is ``/``: - -.. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 11 - :lineno-match: - :language: py +.. note:: -Since this route has a ``pattern`` equaling ``/``, it is the route that will -be matched when the URL ``/`` is visited, e.g., ``http://localhost:6543/``. + Pyramid's :meth:`pyramid.config.Configurator.include` method is the + primary mechanism for extending the configurator and breaking your code + into feature-focused modules. ``main`` next calls the ``scan`` method of the configurator (:meth:`pyramid.config.Configurator.scan`), which will recursively scan our @@ -112,7 +96,7 @@ view configuration will be registered, which will allow one of our application URLs to be mapped to some code. .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 12 + :lines: 11 :lineno-match: :language: py @@ -121,11 +105,41 @@ Finally ``main`` is finished configuring things, so it uses the :term:`WSGI` application: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 - :lineno-start: 13 + :lines: 12 + :lineno-match: :language: py +Route declarations +------------------ + +Open the ``tutorials/routes.py`` file. It should already contain the +following: + +.. literalinclude:: src/basiclayout/tutorial/routes.py + :linenos: + :language: py + +First, on line 2, call :meth:`pyramid.config.Configurator.add_static_view` +with two arguments: ``static`` (the name), and ``static`` (the path). + +This registers a static resource view which will match any URL that starts +with the prefix ``/static`` (by virtue of the first argument to +``add_static_view``). This will serve up static resources for us from within +the ``static`` directory of our ``tutorial`` package, in this case, via +``http://localhost:6543/static/`` and below (by virtue of the second argument +to ``add_static_view``). With this declaration, we're saying that any URL that +starts with ``/static`` should go to the static view; any remainder of its +path (e.g. the ``/foo`` in ``/static/foo``) will be used to compose a path to +a static file resource, such as a CSS file. + +Second, on line 3, the module registers a :term:`route configuration` +via the :meth:`pyramid.config.Configurator.add_route` method that will be +used when the URL is ``/``. Since this route has a ``pattern`` equaling +``/``, it is the route that will be matched when the URL ``/`` is visited, +e.g., ``http://localhost:6543/``. + + View declarations via the ``views`` package ------------------------------------------- diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index fea682628..184f9e1fa 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -46,19 +46,19 @@ 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 ``__init__.py`` file. Any +``add_static_view`` directive we've made in the ``routes.py`` file. Any number and type of static assets can be placed in this directory (or subdirectories) and are just referred to by URL or by using the convenience method ``static_url``, e.g., ``request.static_url(':static/foo.css')`` within templates. -Adding routes to ``__init__.py`` -================================ +Adding routes to ``routes.py`` +============================== This is the URL Dispatch tutorial and so let's start by adding some URL patterns to our app. Later we'll attach views to handle the URLs. -The ``__init__.py`` file contains +The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route` calls which serve to add routes to our application. First, we’ll get rid of the existing route created by the template using the name ``'home'``. It’s only an example and isn’t @@ -66,7 +66,7 @@ relevant to our application. We then need to add four calls to ``add_route``. Note that the *ordering* of these declarations is very important. ``route`` declarations are matched in -the order they're found in the ``__init__.py`` file. +the order they're registered. #. Add a declaration which maps the pattern ``/`` (signifying the root URL) to the route named ``view_wiki``. It maps to our ``view_wiki`` view @@ -91,12 +91,11 @@ the order they're found in the ``__init__.py`` file. attached to the ``edit_page`` view function indicating ``route_name='edit_page'``. -As a result of our edits, the ``__init__.py`` file should look -something like: +As a result of our edits, the ``routes.py`` file should look something like: -.. literalinclude:: src/views/tutorial/__init__.py +.. literalinclude:: src/views/tutorial/routes.py :linenos: - :emphasize-lines: 11-14 + :emphasize-lines: 3-6 :language: python The highlighted lines are the ones that need to be added or edited. diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py index 17763812a..4dab44823 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py @@ -7,7 +7,6 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('home', '/') + config.include('.routes') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py index 17763812a..4dab44823 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py @@ -7,7 +7,6 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('home', '/') + config.include('.routes') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/models/tutorial/routes.py b/docs/tutorials/wiki2/src/models/tutorial/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index 5d8c7fba2..4dab44823 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -7,10 +7,6 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('view_wiki', '/') - config.add_route('view_page', '/{pagename}') - config.add_route('add_page', '/add_page/{pagename}') - config.add_route('edit_page', '/{pagename}/edit_page') + config.include('.routes') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/views/tutorial/routes.py b/docs/tutorials/wiki2/src/views/tutorial/routes.py new file mode 100644 index 000000000..72df58efe --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/routes.py @@ -0,0 +1,6 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('view_page', '/{pagename}') + config.add_route('add_page', '/add_page/{pagename}') + config.add_route('edit_page', '/{pagename}/edit_page') -- cgit v1.2.3 From 00b2c691f88fcf42dfc81222aed939833f7f1f05 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 14 Feb 2016 17:47:48 -0600 Subject: implement the authentication example code --- .../tutorials/wiki2/src/authentication/CHANGES.txt | 4 + .../tutorials/wiki2/src/authentication/MANIFEST.in | 2 + docs/tutorials/wiki2/src/authentication/README.txt | 14 ++ .../wiki2/src/authentication/development.ini | 73 ++++++++++ .../wiki2/src/authentication/production.ini | 62 +++++++++ docs/tutorials/wiki2/src/authentication/setup.py | 49 +++++++ .../wiki2/src/authentication/tutorial/__init__.py | 13 ++ .../src/authentication/tutorial/models/__init__.py | 74 ++++++++++ .../src/authentication/tutorial/models/meta.py | 16 +++ .../src/authentication/tutorial/models/page.py | 20 +++ .../src/authentication/tutorial/models/user.py | 29 ++++ .../wiki2/src/authentication/tutorial/routes.py | 8 ++ .../authentication/tutorial/scripts/__init__.py | 1 + .../tutorial/scripts/initializedb.py | 57 ++++++++ .../wiki2/src/authentication/tutorial/security.py | 28 ++++ .../tutorial/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../src/authentication/tutorial/static/pyramid.png | Bin 0 -> 12901 bytes .../src/authentication/tutorial/static/theme.css | 154 +++++++++++++++++++++ .../authentication/tutorial/static/theme.min.css | 1 + .../authentication/tutorial/templates/404.jinja2 | 8 ++ .../authentication/tutorial/templates/edit.jinja2 | 20 +++ .../tutorial/templates/layout.jinja2 | 64 +++++++++ .../authentication/tutorial/templates/login.jinja2 | 26 ++++ .../authentication/tutorial/templates/view.jinja2 | 18 +++ .../wiki2/src/authentication/tutorial/tests.py | 65 +++++++++ .../src/authentication/tutorial/views/__init__.py | 0 .../src/authentication/tutorial/views/auth.py | 44 ++++++ .../src/authentication/tutorial/views/default.py | 76 ++++++++++ .../src/authentication/tutorial/views/notfound.py | 7 + 29 files changed, 933 insertions(+) create mode 100644 docs/tutorials/wiki2/src/authentication/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/authentication/MANIFEST.in create mode 100644 docs/tutorials/wiki2/src/authentication/README.txt create mode 100644 docs/tutorials/wiki2/src/authentication/development.ini create mode 100644 docs/tutorials/wiki2/src/authentication/production.ini create mode 100644 docs/tutorials/wiki2/src/authentication/setup.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/__init__.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/models/user.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/security.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/tests.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/views/default.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authentication/CHANGES.txt b/docs/tutorials/wiki2/src/authentication/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki2/src/authentication/MANIFEST.in b/docs/tutorials/wiki2/src/authentication/MANIFEST.in new file mode 100644 index 000000000..42cd299b5 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt new file mode 100644 index 000000000..68f430110 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/README.txt @@ -0,0 +1,14 @@ +tutorial README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/python setup.py develop + +- $VENV/bin/initialize_tutorial_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/authentication/development.ini b/docs/tutorials/wiki2/src/authentication/development.ini new file mode 100644 index 000000000..f3079727e --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/development.ini @@ -0,0 +1,73 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = seekrit + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authentication/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini new file mode 100644 index 000000000..686dba48a --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/production.ini @@ -0,0 +1,62 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = real-seekrit + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py new file mode 100644 index 000000000..c342c1aba --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/setup.py @@ -0,0 +1,49 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'bcrypt', + 'docutils', + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + test_suite='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main + """, + ) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py new file mode 100644 index 000000000..f5c033b8b --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.include('.security') + config.scan() + return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py new file mode 100644 index 000000000..a8871f6f5 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py @@ -0,0 +1,74 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines +from .page import Page # flake8: noqa +from .user import User # flake8: noqa + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py new file mode 100644 index 000000000..6fb32a1b2 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py @@ -0,0 +1,29 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash + return False diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/routes.py b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py new file mode 100644 index 000000000..cb747244f --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py @@ -0,0 +1,8 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}') + config.add_route('add_page', '/add_page/{pagename}') + config.add_route('edit_page', '/{pagename}/edit_page') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..f3c0a6fef --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py @@ -0,0 +1,57 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import Page, User + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py new file mode 100644 index 000000000..24035c8b9 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py @@ -0,0 +1,28 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +from .models import User + + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + user = request.user + if user is not None: + return user.id + +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user + +def includeme(config): + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) + + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png differ diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png new file mode 100644 index 000000000..4ab837be9 Binary files /dev/null and b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png differ diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css new file mode 100644 index 000000000..0d25de5b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..37b0a16b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +

+

Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..7db25c674 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 @@ -0,0 +1,20 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} + +{% block content %} +

+Editing {{pagename}} +

+

You can return to the +FrontPage. +

+
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..44d14304e --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 @@ -0,0 +1,64 @@ + + + + + + + + + + + {% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if request.user is none %} +

+ Login +

+ {% else %} +

+ {{request.user.name}} Logout +

+ {% endif %} + {% block content %}{% endblock %} +
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..1806de0ff --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 @@ -0,0 +1,26 @@ +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +

+ + Login +
+{{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..94419e228 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 @@ -0,0 +1,18 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + +{% block content %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {{page.name}}, created by {{page.creator.name}}. +

+

You can return to the +FrontPage. +

+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/tests.py b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py new file mode 100644 index 000000000..c54945c28 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py @@ -0,0 +1,65 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + from .models.meta import Base + + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py new file mode 100644 index 000000000..d3db34132 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py @@ -0,0 +1,44 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..models import User + + +@view_config(route_name='login', renderer='../templates/login.jinja2') +def login(request): + next_url = request.params.get('next', request.referrer) + message = '' + login = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + next_url=next_url, + login=login, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'next': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py new file mode 100644 index 000000000..55aa74d04 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -0,0 +1,76 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPForbidden, + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import view_config + +from ..models import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki') +def view_wiki(request): + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) + +@view_config(route_name='view_page', renderer='../templates/view.jinja2') +def view_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound('No such page') + + def add_link(match): + word = match.group(1) + exists = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '%s' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '%s' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=pagename) + return dict(page=page, content=content, edit_url=edit_url) + +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2') +def edit_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).one() + user = request.user + if user is None or (user.role != 'editor' and page.creator != user): + raise HTTPForbidden + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=pagename), + ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2') +def add_page(request): + user = request.user + if user is None or user.role not in ('editor', 'basic'): + raise HTTPForbidden + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + page.creator = request.user + request.dbsession.add(page) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + save_url = request.route_url('add_page', pagename=pagename) + return dict(pagename=pagename, pagedata='', save_url=save_url) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} -- cgit v1.2.3 From 659a254157c25f9f161f24403a22a2b349d37c67 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 16 Feb 2016 00:18:24 -0600 Subject: add a new authentication chapter --- docs/tutorials/wiki2/authentication.rst | 296 ++++++++++++++++++++++++++++++++ docs/tutorials/wiki2/index.rst | 1 + 2 files changed, 297 insertions(+) create mode 100644 docs/tutorials/wiki2/authentication.rst (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst new file mode 100644 index 000000000..c33ed5138 --- /dev/null +++ b/docs/tutorials/wiki2/authentication.rst @@ -0,0 +1,296 @@ +.. _wiki2_adding_authentication: + +===================== +Adding authentication +===================== + +:app:`Pyramid` provides facilities for :term:`authentication` and +:term:`authorization`. In this section we'll focus solely on the +authentication APIs to add login/logout functionality to our wiki. + +We will implement authentication with the following steps: + +* Add an :term:`authentication policy` and a ``request.user`` computed + property (``security.py``). +* Add routes for /login and /logout (``routes.py``). +* Add login and logout views (``views/auth.py``). +* Add a login template (``login.jinja2``). +* Add "Login" and "Logout" links to every page based on the user's + authenticated state (``layout.jinja2``). +* Make the existing views verify user state (``views/default.py``). +* Redirect to /login when a user is denied access to any of the views + that require permission, instead of a default "403 Forbidden" page + (``views/auth.py``). + +Authenticating requests +----------------------- + +The core of :app:`Pyramid` authentication is a :term:`authentication policy` +which is used to identify authentication information from a ``request``, +as well as handling the low-level login/logout operations required to +track users across requests (via cookies or headers or whatever else you can +imagine). + +Add the authentication policy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file ``tutorial/security.py``: + +.. literalinclude:: src/authentication/tutorial/security.py + :linenos: + :emphasize-lines: 9,14,28 + :language: python + +Here we've defined: + +* A new authentication policy named ``MyAuthenticationPolicy`` which is + subclassed from pyramid's + :class:`pyramid.authentication.AuthTktAuthenticationPolicy` which tracks + the :term:`userid` using a signed cookie. +* A ``get_user`` function which can convert the ``unauthenticated_userid`` + from the policy into a ``User`` object from our database. +* Finally, the ``get_user`` is registered on the request as ``request.user`` + to be used throughout our application as the authenticated ``User`` object + for the logged-in user. + +The logic in this file is a little bit interesting and so we'll go into +detail about what's happening here: + +First, the default authentication policies all provide a method named +``unauthenticated_userid`` which is responsible for the low-level parsing +of the information in the request (cookies, headers, etc). If a ``userid`` +is found then it is returned from this method. This is named +``unauthenticated_userid`` because at the lowest level it knows the value of +the userid in the cookie but it doesn't know if it's actually a user in our +system (remember, anything the user sends to our app is untrusted). + +Second, our application should only care about ``authenticated_userid`` and +``request.user`` which have gone through our application-specific process of +validating that the user is logged-in. + +In order to provide an ``authenticated_userid`` we need a verification step. +That can happen anywhere, so we've elected to do it inside of the cached +``request.user`` computed property. This is a convenience that makes +``request.user`` the source of truth in our system. It is either ``None`` or +a ``User`` object from our database. This is why the ``get_user`` function +uses the ``unauthenticated_userid`` to check the database + +Configure the app +~~~~~~~~~~~~~~~~~ + +Since we've added a new ``tutorial/security.py`` module we need to include it. + +Open the file ``tutorial/__init__.py`` and edit the following lines: + +.. literalinclude:: src/authentication/tutorial/__init__.py + :linenos: + :emphasize-lines: 11 + :language: python + +Our authentication policy is expecting a new setting, ``auth.secret``. Open +the file ``development.ini`` and add the highlighted line below: + +.. literalinclude:: src/authentication/development.ini + :lines: 18-20 + :emphasize-lines: 3 + :lineno-match: + :language: ini + +Finally best-practices tell us to use a different secret for production so +open ``production.ini`` and add a different secret: + +.. literalinclude:: src/authentication/production.ini + :lines: 15-17 + :emphasize-lines: 3 + :lineno-match: + :language: ini + +Add permission checks +~~~~~~~~~~~~~~~~~~~~~ + +:app:`Pyramid` has full support for declarative authorization which we'll +cover in the next chapter. However many people looking to get their feet +wet are just interested in authentication with some basic form of +home-grown authorization. We'll show below how to accomplish the simple +security goals of our wiki now that we can track the logged-in state of users. + +Remember our goals: + +* Allow only ``editor`` and ``basic`` logged-in users to create new pages. +* Only allow ``editor`` users and the page creator (possibly a ``basic`` user) + to edit pages. + +Open the file ``tutorial/views/default.py`` and fix the following imports: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 5-13 + :lineno-match: + :emphasize-lines: 2,9 + :language: python + +Only the highlighted lines need to be changed. + +Now edit the ``add_page`` view function: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 62-76 + :lineno-match: + :emphasize-lines: 3-5,10 + :language: python + +Only the highlighted lines need to be changed. + +If the user is not logged in or is not in the ``basic`` or ``editor`` roles +then we raise ``HTTPForbidden`` which will return a "403 Forbidden" response +to the user. However we hook this later to redirect to the login page. Also, +now that we have ``request.user`` we no longer have to hard-code the creator +as the ``editor`` user so we can finally drop that hack. + +Now edit the ``edit_page`` view function: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 45-60 + :lineno-match: + :emphasize-lines: 5-7 + :language: python + +Only the highlighted lines need to be changed. + +If the user is not logged in or the user is not the page's creator **and** +not an ``editor`` then we raise ``HTTPForbidden``. + +These simple checks should protect our views. + +Login, logout +------------- + +Now that we've got the ability to detect logged-in users, we need to +add the /login and /logout views so that they can actually login! + +Add routes for /login and /logout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Go back to ``tutorial/routes.py`` and add these two routes as highlighted: + +.. literalinclude:: src/authentication/tutorial/routes.py + :lines: 3-6 + :lineno-match: + :emphasize-lines: 2-3 + :language: python + +.. note:: The preceding lines must be added *before* the following + ``view_page`` route definition: + + .. literalinclude:: src/authentication/tutorial/routes.py + :lines: 6 + :language: python + + This is because ``view_page``'s route definition uses a catch-all + "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`) + which will catch any route that was not already caught by any route + registered before it. Hence, for ``login`` and ``logout`` views to + have the opportunity of being matched (or "caught"), they must be above + ``/{pagename}``. + +Add login, logout and forbidden views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file ``tutorial/views/auth.py`` where we will add the following +code: + +.. literalinclude:: src/authentication/tutorial/views/auth.py + :linenos: + :language: python + +This code adds 3 new views to application: + +- The ``login`` view renders a login form and processes the post from the + login form, checking credentials against our ``users`` table in the database. + + The check is done by first finding a ``User`` record in the database and + then using our ``user.check_password`` method to compare the passwords. + + If the credentials are valid then we use our authentication policy to + store the user's id in the response using :meth:`pyramid.security.remember`. + + Finally, the user is redirected back to the page they were trying to access + (``next``) or the front page as a fallback. This parameter is used by + our forbidden view as explained below to finish the login workflow. + +- The ``logout`` view handles requests to /logout by clearing the credentials + using :meth:`pyramid.security.forget` and then redirecting them to the front + page. + +- The ``forbidden_view`` is registered using the + :class:`pyramid.view.forbidden_view_config` decorator. This is a special + :term:`exception view` which is invoked when a + :class:`pyramid.httpexceptions.HTTPForbidden` exception is raised. + + This view will handle a forbidden error by redirecting the user to /login. + As a convenience it also sets the ``next=`` query string to the current url + (the one that is forbidding access). This way if the user successfully logs + in they will be sent back to the page they had been trying to access. + +Add the ``login.jinja2`` template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create ``tutorial/templates/login.jinja2`` with the following content: + +.. literalinclude:: src/authentication/tutorial/templates/login.jinja2 + :language: html + +The above template is referenced in the login view that we just added in +``tutorial/views/auth.py``. + +Add a "Login" and "Logout" links +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Open ``tutorial/templates/layout.jinja2`` and add the following code as +indicated by the highlighted lines. + +.. literalinclude:: src/authentication/tutorial/templates/layout.jinja2 + :lines: 35-46 + :lineno-match: + :emphasize-lines: 2-10 + :language: html + +The ``request.user`` will be ``None`` if the user is not authenticated, or a +``tutorial.models.User`` object if the user is authenticated. This +check will make the logout link active only when the user is logged in and +vice versa the login link is only active when the user is logged out. + +Viewing the application in a browser +------------------------------------ + +We can finally examine our application in a browser (See +:ref:`wiki2-start-the-application`). Launch a browser and visit each of the +following URLs, checking that the result is as expected: + +- http://localhost:6543/ invokes the ``view_wiki`` view. This always + redirects to the ``view_page`` view of the ``FrontPage`` page object. It + is executable by any user. + +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` page object. There is a "Login" link in the upper right corner. + +- http://localhost:6543/FrontPage/edit_page invokes the edit view for the + FrontPage object. It is executable by only the ``editor`` user. If a + different user (or the anonymous user) invokes it, a login form will be + displayed. Supplying the credentials with the username ``editor``, password + ``editor`` will display the edit page form. + +- http://localhost:6543/add_page/SomePageName invokes the add view for a page. + It is executable by the ``editor`` or ``basic`` user. If a different user + (or the anonymous user) invokes it, a login form will be displayed. Supplying + the credentials with the username ``basic``, password ``basic`` will display + the edit page form. + +- http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` + if the page was created by that user in the previous step. If, instead, the + page was created by ``editor`` then the login page should be shown for the + ``basic`` user. + +- After logging in (as a result of hitting an edit or add page and submitting + the login form with the ``editor`` credentials), we'll see a Logout link in + the upper right hand corner. When we click it, we're logged out, and + redirected back to the front page. diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 0a3873dcd..74fb5bfd5 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -22,6 +22,7 @@ which corresponds to the same location if you have Pyramid sources. basiclayout definingmodels definingviews + authentication authorization tests distributing -- cgit v1.2.3 From 38b40761f1ba31773aca64ae600428516c98534c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 16 Feb 2016 23:23:08 -0600 Subject: use page.name to prepare for context --- docs/tutorials/wiki2/src/authentication/tutorial/views/default.py | 6 +++--- docs/tutorials/wiki2/src/views/tutorial/views/default.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py index 55aa74d04..ffe967f6e 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -39,7 +39,7 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(add_link, content) - edit_url = request.route_url('edit_page', pagename=pagename) + edit_url = request.route_url('edit_page', pagename=page.name) return dict(page=page, content=content, edit_url=edit_url) @view_config(route_name='edit_page', renderer='../templates/edit.jinja2') @@ -51,12 +51,12 @@ def edit_page(request): raise HTTPForbidden if 'form.submitted' in request.params: page.data = request.params['body'] - next_url = request.route_url('view_page', pagename=pagename) + next_url = request.route_url('view_page', pagename=page.name) return HTTPFound(location=next_url) return dict( pagename=page.name, pagedata=page.data, - save_url=request.route_url('edit_page', pagename=pagename), + save_url=request.route_url('edit_page', pagename=page.name), ) @view_config(route_name='add_page', renderer='../templates/edit.jinja2') diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index 7a4073b3f..c1d402f6a 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -38,7 +38,7 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(add_link, content) - edit_url = request.route_url('edit_page', pagename=pagename) + edit_url = request.route_url('edit_page', pagename=page.name) return dict(page=page, content=content, edit_url=edit_url) @view_config(route_name='edit_page', renderer='../templates/edit.jinja2') @@ -47,12 +47,12 @@ def edit_page(request): page = request.dbsession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - next_url = request.route_url('view_page', pagename=pagename) + next_url = request.route_url('view_page', pagename=page.name) return HTTPFound(location=next_url) return dict( pagename=page.name, pagedata=page.data, - save_url=request.route_url('edit_page', pagename=pagename), + save_url=request.route_url('edit_page', pagename=page.name), ) @view_config(route_name='add_page', renderer='../templates/edit.jinja2') -- cgit v1.2.3 From f2c43689b50152d55ddc98e8f56754ee61f9a8c7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 16 Feb 2016 23:23:23 -0600 Subject: remove whitespace --- docs/tutorials/wiki2/authentication.rst | 2 +- docs/tutorials/wiki2/src/authentication/tutorial/security.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index c33ed5138..0b5e71099 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -38,7 +38,7 @@ Create a new file ``tutorial/security.py``: .. literalinclude:: src/authentication/tutorial/security.py :linenos: - :emphasize-lines: 9,14,28 + :emphasize-lines: 9,14,21-27 :language: python Here we've defined: diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py index 24035c8b9..8ea3858d2 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/security.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py @@ -22,7 +22,6 @@ def includeme(config): settings['auth.secret'], hashalg='sha512', ) - config.set_authentication_policy(authn_policy) config.set_authorization_policy(ACLAuthorizationPolicy()) config.add_request_method(get_user, 'user', reify=True) -- cgit v1.2.3 From 2fa90465bfdd213b6ce51ca8de6eaf9b614c283e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 16 Feb 2016 23:42:04 -0600 Subject: add first cut at source for authorization chapter --- .../wiki2/src/authorization/development.ini | 2 + .../wiki2/src/authorization/production.ini | 2 + .../wiki2/src/authorization/tutorial/__init__.py | 8 +--- .../src/authorization/tutorial/models/user.py | 6 ++- .../wiki2/src/authorization/tutorial/routes.py | 50 ++++++++++++++++++++ .../wiki2/src/authorization/tutorial/security.py | 51 ++++++++------------- .../authorization/tutorial/templates/edit.jinja2 | 6 +-- .../authorization/tutorial/templates/layout.jinja2 | 10 ++-- .../authorization/tutorial/templates/login.jinja2 | 4 +- .../authorization/tutorial/templates/view.jinja2 | 4 +- .../wiki2/src/authorization/tutorial/views/auth.py | 23 ++++------ .../src/authorization/tutorial/views/default.py | 53 ++++++++++------------ 12 files changed, 126 insertions(+), 93 deletions(-) create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/routes.py (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index 99c4ff0fe..f3079727e 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -17,6 +17,8 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +auth.secret = seekrit + # By default, the toolbar only appears for clients from IP addresses # '127.0.0.1' and '::1'. # debugtoolbar.hosts = 127.0.0.1 ::1 diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index cb1db3211..686dba48a 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -14,6 +14,8 @@ pyramid.default_locale_name = en sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +auth.secret = real-seekrit + [server:main] use = egg:waitress#main host = 0.0.0.0 diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 8eacdee5a..f5c033b8b 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -7,13 +7,7 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') + config.include('.routes') config.include('.security') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('view_wiki', '/') - config.add_route('login', '/login') - config.add_route('logout', '/logout') - config.add_route('view_page', '/{pagename}') - config.add_route('add_page', '/add_page/{pagename}') - config.add_route('edit_page', '/{pagename}/edit_page') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py index 25b0a8187..6fb32a1b2 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py @@ -18,10 +18,12 @@ class User(Base): password_hash = Column(Text) def set_password(self, pw): - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): if self.password_hash is not None: - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py new file mode 100644 index 000000000..c7c3a2120 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py @@ -0,0 +1,50 @@ +from pyramid.httpexceptions import HTTPNotFound +from pyramid.security import ( + Allow, + Everyone, +) + +from .models import Page + +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}', factory=page_factory) + config.add_route('add_page', '/add_page/{pagename}', + factory=new_page_factory) + config.add_route('edit_page', '/{pagename}/edit_page', + factory=page_factory) + +def new_page_factory(request): + pagename = request.matchdict['pagename'] + return NewPage(pagename) + +class NewPage(object): + def __init__(self, pagename): + self.pagename = pagename + + def __acl__(self): + return [ + (Allow, 'role:editor', 'create'), + (Allow, 'role:basic', 'create'), + ] + +def page_factory(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound + return PageResource(page) + +class PageResource(object): + def __init__(self, page): + self.page = page + + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, 'role:editor', 'edit'), + (Allow, str(self.page.creator_id), 'edit'), + ] diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py index 7bceabf3f..25cff7b05 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py @@ -1,51 +1,40 @@ from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy - from pyramid.security import ( - Allow, Authenticated, Everyone, ) +from .models import User -USERS = { - 'editor': 'editor', - 'viewer': 'viewer', -} - -GROUPS = { - 'editor': ['group:editors'], -} class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): def authenticated_userid(self, request): - userid = self.unauthenticated_userid(request) - if userid in USERS: - return userid + user = request.user + if user is not None: + return user.id def effective_principals(self, request): principals = [Everyone] - userid = self.authenticated_userid(request) - if userid is not None: + user = request.user + if user is not None: principals.append(Authenticated) - principals.append(userid) - - groups = GROUPS.get(userid, []) - principals.extend(groups) + principals.append(str(user.id)) + principals.append('role:' + user.role) return principals -class RootFactory(object): - __acl__ = [ - (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit'), - ] - - def __init__(self, request): - pass +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user def includeme(config): - authn_policy = MyAuthenticationPolicy('sosecret', hashalg='sha512') - authz_policy = ACLAuthorizationPolicy() - config.set_root_factory(RootFactory) + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) config.set_authentication_policy(authn_policy) - config.set_authorization_policy(authz_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 index e47b3aabf..7db25c674 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -1,17 +1,17 @@ {% extends 'layout.jinja2' %} -{% block title %}Edit {{page.name}} - {% endblock title %} +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} {% block content %}

-Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +Editing {{pagename}}

You can return to the FrontPage.

- +
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 index 82a144abf..44d14304e 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 @@ -8,7 +8,7 @@ - {% block title %}{% if page.name %} {{page.name}} - {% endif %}{% endblock title %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + {% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) @@ -33,9 +33,13 @@
- {% if request.authenticated_userid is not none %} + {% if request.user is none %}

- Logout + Login +

+ {% else %} +

+ {{request.user.name}} Logout

{% endif %} {% block content %}{% endblock %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 index 99d369173..1806de0ff 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 @@ -10,14 +10,14 @@ {{ message }}

- +
- +
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 index c582ce1f9..94419e228 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -1,5 +1,7 @@ {% extends 'layout.jinja2' %} +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + {% block content %}

{{ content|safe }}

@@ -8,7 +10,7 @@

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} + Viewing {{page.name}}, created by {{page.creator.name}}.

You can return to the FrontPage. diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py index 08aa2bfad..d3db34132 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py @@ -8,33 +8,28 @@ from pyramid.view import ( view_config, ) -from ..security.default import USERS +from ..models import User -@view_config(route_name='login', renderer='templates/login.jinja2') +@view_config(route_name='login', renderer='../templates/login.jinja2') def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) + next_url = request.params.get('next', request.referrer) message = '' login = '' - password = '' if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, headers=headers) + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) message = 'Failed login' return dict( message=message, url=request.route_url('login'), - came_from=came_from, + next_url=next_url, login=login, - password=password, ) @view_config(route_name='logout') @@ -45,5 +40,5 @@ def logout(request): @forbidden_view_config() def forbidden_view(request): - next_url = request.route_url('login', _query={'came_from': request.url}) + next_url = request.route_url('login', _query={'next': request.url}) return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index f74059be0..9358993ea 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -2,19 +2,15 @@ import cgi import re from docutils.core import publish_parts -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) +from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from ..models import Page - # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki', permission='view') +@view_config(route_name='view_wiki') def view_wiki(request): next_url = request.route_url('view_page', pagename='FrontPage') return HTTPFound(location=next_url) @@ -22,12 +18,9 @@ def view_wiki(request): @view_config(route_name='view_page', renderer='../templates/view.jinja2', permission='view') def view_page(request): - pagename = request.matchdict['pagename'] - page = request.dbsession.query(Page).filter_by(name=pagename).first() - if page is None: - raise HTTPNotFound('No such page') + page = request.context.page - def check(match): + def add_link(match): word = match.group(1) exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: @@ -38,34 +31,34 @@ def view_page(request): return '%s' % (add_url, cgi.escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=page.name) return dict(page=page, content=content, edit_url=edit_url) +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', + permission='edit') +def edit_page(request): + page = request.context.page + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=page.name) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=page.name), + ) + @view_config(route_name='add_page', renderer='../templates/edit.jinja2', permission='create') def add_page(request): - pagename = request.matchdict['pagename'] + pagename = request.context.pagename if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) + page.creator = request.user request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url) - -@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', - permission='edit') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = request.dbsession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - next_url = request.route_url('view_page', pagename=pagename) - return HTTPFound(location=next_url) - return dict( - page=page, - save_url=request.route_url('edit_page', pagename=pagename), - ) + return dict(pagename=pagename, pagedata='', save_url=save_url) -- cgit v1.2.3 From 9e85d2bf9489fff46ec7ea47b79bebcdc19d9a8e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Feb 2016 01:18:20 -0600 Subject: update the authorization chapter --- docs/tutorials/wiki2/authentication.rst | 2 +- docs/tutorials/wiki2/authorization.rst | 418 +++++++++++--------------------- 2 files changed, 147 insertions(+), 273 deletions(-) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 0b5e71099..1b18e5c55 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -49,7 +49,7 @@ Here we've defined: the :term:`userid` using a signed cookie. * A ``get_user`` function which can convert the ``unauthenticated_userid`` from the policy into a ``User`` object from our database. -* Finally, the ``get_user`` is registered on the request as ``request.user`` +* The ``get_user`` is registered on the request as ``request.user`` to be used throughout our application as the authenticated ``User`` object for the logged-in user. diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 1ee5cc714..eb9269dff 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -4,342 +4,216 @@ Adding authorization ==================== -:app:`Pyramid` provides facilities for :term:`authentication` and -:term:`authorization`. We'll make use of both features to provide security -to our application. Our application currently allows anyone with access to -the server to view, edit, and add pages to our wiki. We'll change that to -allow only people who are members of a *group* named ``group:editors`` to add -and edit wiki pages but we'll continue allowing anyone with access to the -server to view pages. - -We will also add a login page and a logout link on all the pages. The login -page will be shown when a user is denied access to any of the views that -require permission, instead of a default "403 Forbidden" page. - -We will implement the access control with the following steps: - -* Add users and groups (``security/default.py``, a new subpackage). -* Add an :term:`ACL` (``models/mymodel.py`` and ``__init__.py``). -* Add an :term:`authentication policy` and an :term:`authorization policy` - (``__init__.py``). -* Add :term:`permission` declarations to the ``edit_page`` and ``add_page`` - views (``views/default.py``). - -Then we will add the login and logout feature: - -* Add routes for /login and /logout (``__init__.py``). -* Add ``login`` and ``logout`` views (``views/default.py``). -* Add a login template (``login.jinja2``). -* Make the existing views return a ``logged_in`` flag to the renderer +In the last chapter we built :term:`authentication` into our wiki2. We also +went one step further and used the ``request.user`` object to perform some explicit :term:`authorization` checks. This is fine for a lot of +applications but :app:`Pyramid` provides some facilities for cleaning this +up and decoupling the constraints from the view function itself. + +We will implement access control with the following steps: + +* Update the :term:`authentication policy` to break down the + :term:`userid` into a list of :term:`principals ` + (``security.py``). +* Define an :term:`authorization policy` for mapping users, resources and + permissions (``security.py``). +* Add new :term:`resource` definitions that will be used as the + :term:`context` for the wiki pages (``routes.py``). +* Add an :term:`ACL` to each resource (``routes.py``). +* Replace the inline checks on the views with :term:`permission` declarations (``views/default.py``). -* Add a "Logout" link to be shown when logged in and viewing or editing a page - (``view.jinja2``, ``edit.jinja2``). +Add user principals +------------------- -Access control --------------- +A :term:`principal` is a level of abstraction on top of the raw +:term:`userid` that describes the user in terms of capabilities, roles or +other identifiers that are easier to generalize. The permissions are then +written against the principals without focusing on the exact user involved. -Add users and groups -~~~~~~~~~~~~~~~~~~~~ +:app:`Pyramid` defines two builtin principals used in every application: +:attr:`pyramid.security.Everyone` and :attr:`pyramid.security.Authenticated`. +On top of these we have already mentioned the required principals for this +application in the original design. The user has two possible roles: +``editor`` and ``basic``. These will be prefixed by the ``role:`` +string to avoid clasing with any other types of principals. -Create a new ``tutorial/security/default.py`` subpackage with the -following content: +Open the file ``tutorial/security.py`` and edit the following lines: -.. literalinclude:: src/authorization/tutorial/security/default.py +.. literalinclude:: src/authorization/tutorial/security.py :linenos: + :emphasize-lines: 3-6,17-24 :language: python -The ``groupfinder`` function accepts a userid and a request and -returns one of these values: - -- If the userid exists in the system, it will return a sequence of group - identifiers (or an empty sequence if the user isn't a member of any groups). -- If the userid *does not* exist in the system, it will return ``None``. - -For example, ``groupfinder('editor', request )`` returns ``['group:editor']``, -``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin', -request)`` returns ``None``. We will use ``groupfinder()`` as an -:term:`authentication policy` "callback" that will provide the -:term:`principal` or principals for a user. - -In a production system, user and group data will most often come from a -database, but here we use "dummy" data to represent user and groups sources. +Only the highlighted lines need to be added. -Add an ACL -~~~~~~~~~~ +Note that the role comes from the ``User`` object and finally we also +add the ``user.id`` as a principal for when we want to allow that exact +user to edit page's they've created. -Open ``tutorial/models/mymodel.py`` and add the following import -statement at the top: +Add the authorization policy +---------------------------- -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 1-4 - :language: python +We already added the :term:`authorization policy` in the previous chapter +because :app:`Pyramid` requires one when adding an +:term:`authentication policy`. However, it was not used anywhere and so we'll +mention it now. -Add the following class definition at the end: +Open the file ``tutorial/security.py`` and notice the following lines: -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 22-29 +.. literalinclude:: src/authorization/tutorial/security.py + :lines: 38-40 + :lineno-match: + :emphasize-lines: 2 :language: python -We import :data:`~pyramid.security.Allow`, an action that means that -permission is allowed, and :data:`~pyramid.security.Everyone`, a special -:term:`principal` that is associated to all requests. Both are used in the -:term:`ACE` entries that make up the ACL. - -The ACL is a list that needs to be named `__acl__` and be an attribute of a -class. We define an :term:`ACL` with two :term:`ACE` entries. The first entry -allows any user (``Everyone``) the `view` permission. The second entry allows -the ``group:editors`` principal the `edit` permission. +We're using the :class:`pyramid.authorization.ACLAuthorizationPolicy` which +will suffice for most applications. It uses the :term:`context` to define +the mapping between a :term:`principal` and :term:`permission` for the +current request via the ``__acl__``. -The ``RootFactory`` class that contains the ACL is a :term:`root factory`. We -need to associate it to our :app:`Pyramid` application, so the ACL is provided -to each view in the :term:`context` of the request as the ``context`` -attribute. +Add resources and ACLs +---------------------- -Open ``tutorial/__init__.py`` and define a new root factory using -:meth:`pyramid.config.Configurator.set_root_factory` using the class that we -created above: +Resources are the hidden gem of :app:`Pyramid`. You've made it! -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 14-17 - :emphasize-lines: 17 - :language: python +Every URL in a web application is representing a :term:`resource` +(the **R** in Uniform Resource Locator). Often the resource is something +in your data model but it could also be an abstraction over many models. -Only the highlighted line needs to be added. +Our wiki has two resources: -We are now providing the ACL to the application. See :ref:`assigning_acls` -for more information about what an :term:`ACL` represents. +#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited. + Only ``editor`` users as well as the original creator of the ``Page`` + may edit the ``PageResource`` but anyone may view it. -.. note:: Although we don't use the functionality here, the ``factory`` used - to create route contexts may differ per-route as opposed to globally. See - the ``factory`` argument to :meth:`pyramid.config.Configurator.add_route` - for more info. +#. A ``NewPage``. Represents a potential ``Page`` that does not exist. + Any logged-in user (roles ``basic`` or ``editor``) can create pages. -Add authentication and authorization policies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: -Open ``tutorial/__init__.py`` and add the highlighted import -statements: + The wiki data model is simple enough that the ``PageResource`` is + actually mostly redundant with our ``models.Page`` SQLAlchemy class. It is + completely valid to combine these into one class. However, for this + tutorial they are explicitly separated to make it clear the + parts that :app:`Pyramid` cares about versus application-defined objects. -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 1-5 - :emphasize-lines: 2-5 - :language: python +There are many ways to define these resources, and they can even be grouped +into collections with a hierarchy. However, we're keeping it simple here! -Now add those policies to the configuration: +Open the file ``tutorial/routes.py`` and edit the following lines: -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 11-19 - :emphasize-lines: 1-3,8-9 +.. literalinclude:: src/authorization/tutorial/routes.py + :linenos: + :emphasize-lines: 1-7,14-50 :language: python -Only the highlighted lines need to be added. - -We are enabling an ``AuthTktAuthenticationPolicy``, which is based in an auth -ticket that may be included in the request. We are also enabling an -``ACLAuthorizationPolicy``, which uses an ACL to determine the *allow* or -*deny* outcome for a view. - -Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy` -constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is -a string representing an encryption key used by the "authentication ticket" -machinery represented by this policy; it is required. The ``callback`` is the -``groupfinder()`` function that we created before. +The highlighted lines need to be edited or added. +The ``NewPage`` has an ``__acl__`` on it that returns a list of +mappings from :term:`principal` to :term:`permission`. This defines **who** +can do **what** with that :term:`resource`. In our case we want to only +allow users with the principals ``role:editor`` and ``role:basic`` to +have the ``create`` permission: -Add permission declarations -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Open ``tutorial/views/default.py`` and add a ``permission='view'`` -parameter to the ``@view_config`` decorator for ``view_wiki()`` and -``view_page()`` as follows: - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 24-25 - :emphasize-lines: 1 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 20-32 + :lineno-match: + :emphasize-lines: 11,12 :language: python -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 29-31 - :emphasize-lines: 1-2 - :language: python - -Only the highlighted lines, along with their preceding commas, need to be -edited and added. - -This allows anyone to invoke these two views. +The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` +route by declaring a ``factory`` on the route: -Add a ``permission='edit'`` parameter to the ``@view_config`` decorators for -``add_page()`` and ``edit_page()``: - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 52-54 - :emphasize-lines: 1-2 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 15-16 + :lineno-match: + :emphasize-lines: 2 :language: python -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 66-68 - :emphasize-lines: 1-2 - :language: python +The ``PageResource`` defines the :term:`ACL` for a ``Page``. It uses an +actual ``Page`` object to determine **who** can do **what** to the page. -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. - -We are done with the changes needed to control access. The changes that -follow will add the login and logout feature. - -Login, logout -------------- - -Add routes for /login and /logout -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Go back to ``tutorial/__init__.py`` and add these two routes as -highlighted: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 21-24 - :emphasize-lines: 2-3 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 34-50 + :lineno-match: + :emphasize-lines: 14-16 :language: python -.. note:: The preceding lines must be added *before* the following - ``view_page`` route definition: - - .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 24 - :language: python +The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` +and ``edit_page`` route by declaring a ``factory`` on the routes: - This is because ``view_page``'s route definition uses a catch-all - "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`) - which will catch any route that was not already caught by any route listed - above it in ``__init__.py``. Hence, for ``login`` and ``logout`` views to - have the opportunity of being matched (or "caught"), they must be above - ``/{pagename}``. +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 14-18 + :lineno-match: + :emphasize-lines: 1,4-5 + :language: python -Add login and logout views -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add view permissions +-------------------- -We'll add a ``login`` view which renders a login form and processes the post -from the login form, checking credentials. +At this point we've modified our application to load the ``PageResource``, +including the actual ``Page`` model in the ``page_factory``. The +``PageResource`` is now the :term:`context` for all ``view_page`` and +``edit_page`` views. Similarly the ``NewPage`` will be the context for +the ``add_page`` view. -We'll also add a ``logout`` view callable to our application and provide a -link to it. This view will clear the credentials of the logged in user and -redirect back to the front page. +Open the file ``views/default.py``. -Add the following import statements to ``tutorial/views/default.py`` -after the import from ``pyramid.httpexceptions``: +First, you can drop a few imports that are no longer necessary: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 9-19 - :emphasize-lines: 1-8,11 + :lines: 5-7 + :lineno-match: + :emphasize-lines: 1 :language: python -All the highlighted lines need to be added or edited. - -:meth:`~pyramid.view.forbidden_view_config` will be used to customize the -default 403 Forbidden page. :meth:`~pyramid.security.remember` and -:meth:`~pyramid.security.forget` help to create and expire an auth ticket -cookie. - -Now add the ``login`` and ``logout`` views at the end of the file: +Edit the ``view_page`` view to declare the ``view`` permission and remove +the explicit checks within the view: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 81-112 - :language: python - -``login()`` has two decorators: - -- a ``@view_config`` decorator which associates it with the ``login`` route - and makes it visible when we visit ``/login``, and -- a ``@forbidden_view_config`` decorator which turns it into a - :term:`forbidden view`. ``login()`` will be invoked when a user tries to - execute a view callable for which they lack authorization. For example, if - a user has not logged in and tries to add or edit a wiki page, they will be - shown the login form before being allowed to continue. - -The order of these two :term:`view configuration` decorators is unimportant. - -``logout()`` is decorated with a ``@view_config`` decorator which associates -it with the ``logout`` route. It will be invoked when we visit ``/logout``. - -Add the ``login.jinja2`` template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Create ``tutorial/templates/login.jinja2`` with the following content: - -.. literalinclude:: src/authorization/tutorial/templates/login.jinja2 - :language: html - -The above template is referenced in the login view that we just added in -``views/default.py``. - -Add a "Logout" link when logged in -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Open ``tutorial/templates/edit.jinja2`` and -``tutorial/templates/view.jinja2`` and add the following code as -indicated by the highlighted lines. - -.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 - :lines: 34-40 - :emphasize-lines: 3-7 - :language: html - -The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if -the user is not authenticated, or a userid if the user is authenticated. This -check will make the logout link active only when the user is logged in. - -Reviewing our changes ---------------------- - -Our ``tutorial/__init__.py`` will look like this when we're done: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :linenos: - :emphasize-lines: 2-3,5,11-13,17-19,22-23 + :lines: 18-23 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. +The work of loading the page has already been done in the factory so we +can just pull the ``page`` object out of the ``PageResource`` loaded as +``request.context``. Our factory also guarantees we will have a ``Page`` as it +raises ``HTTPNotFound`` otherwise - again simplifying the view logic. -Our ``tutorial/models/mymodel.py`` will look like this when we're done: +Edit the ``edit_page`` view to declare the ``edit`` permission: -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :linenos: - :emphasize-lines: 1-4,22-29 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 38-42 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. - -Our ``tutorial/views/default.py`` will look like this when we're done: +Edit the ``add_page`` view to declare the ``create`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py - :linenos: - :emphasize-lines: 9-16,19,24,29-30,52-53,66-67,81-112 + :lines: 52-56 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. - -Our ``tutorial/templates/edit.jinja2`` template will look like this when -we're done: - -.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 - :linenos: - :emphasize-lines: 36-40 - :language: html +Note the ``pagename`` here is pulled off of the context instead of +``request.matchdict``. The factory has done a lot of work for us to hide the +actual route pattern. -Only the highlighted lines need to be added or edited. +The ACLs defined on each :term:`resource` are used by the +:term:`authorization policy` to determine if any +:term:`principal` is allowed to have some :term:`permission`. If this check +fails (for example, the user is not logged in) then a ``HTTPForbidden`` +exception will be raised automatically, thus we're able to drop those +exceptions and checks from the views themselves. Rather we've defined them in +terms of operations on a resource. -Our ``tutorial/templates/view.jinja2`` template will look like this when -we're done: +The final ``tutorial/views/default.py`` should look like the following: -.. literalinclude:: src/authorization/tutorial/templates/view.jinja2 +.. literalinclude:: src/authorization/tutorial/views/default.py :linenos: - :emphasize-lines: 36-40 - :language: html - -Only the highlighted lines need to be added or edited. + :language: python Viewing the application in a browser ------------------------------------ -- cgit v1.2.3 From 91f7ed469664bf71f98b6e55ea096f5bdddae953 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Feb 2016 01:53:49 -0600 Subject: add webtest and tests_require to setup.py --- docs/tutorials/wiki2/src/authentication/setup.py | 5 +++++ docs/tutorials/wiki2/src/authorization/setup.py | 5 +++++ docs/tutorials/wiki2/src/basiclayout/setup.py | 5 +++++ docs/tutorials/wiki2/src/models/setup.py | 5 +++++ docs/tutorials/wiki2/src/views/setup.py | 5 +++++ 5 files changed, 25 insertions(+) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py index c342c1aba..57538f2d0 100644 --- a/docs/tutorials/wiki2/src/authentication/setup.py +++ b/docs/tutorials/wiki2/src/authentication/setup.py @@ -21,6 +21,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -39,6 +43,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index c342c1aba..57538f2d0 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -21,6 +21,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -39,6 +43,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index eb771010f..7bc697730 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -19,6 +19,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -37,6 +41,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index df9fec4d4..bdc9ceed7 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -20,6 +20,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -38,6 +42,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index c342c1aba..57538f2d0 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -21,6 +21,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -39,6 +43,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] -- cgit v1.2.3 From 50e08a743d097616ef7f76c9689833eab215cb94 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Feb 2016 02:22:26 -0600 Subject: add fallback for next_url --- docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py | 2 ++ docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py | 2 ++ 2 files changed, 4 insertions(+) (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py index d3db34132..2b993b430 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py @@ -14,6 +14,8 @@ from ..models import User @view_config(route_name='login', renderer='../templates/login.jinja2') def login(request): next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') message = '' login = '' if 'form.submitted' in request.params: diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py index d3db34132..2b993b430 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py @@ -14,6 +14,8 @@ from ..models import User @view_config(route_name='login', renderer='../templates/login.jinja2') def login(request): next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') message = '' login = '' if 'form.submitted' in request.params: -- cgit v1.2.3 From 66fabb4ac707b5b4289db0094756f1a1af7269cc Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Feb 2016 02:32:08 -0600 Subject: update tests chapter --- docs/tutorials/wiki2/src/tests/development.ini | 2 + docs/tutorials/wiki2/src/tests/production.ini | 2 + docs/tutorials/wiki2/src/tests/setup.py | 2 +- .../tutorials/wiki2/src/tests/tutorial/__init__.py | 19 +--- .../wiki2/src/tests/tutorial/models/user.py | 6 +- docs/tutorials/wiki2/src/tests/tutorial/routes.py | 50 +++++++++++ .../tutorials/wiki2/src/tests/tutorial/security.py | 40 +++++++++ .../wiki2/src/tests/tutorial/security/__init__.py | 0 .../wiki2/src/tests/tutorial/security/default.py | 12 --- .../wiki2/src/tests/tutorial/templates/edit.jinja2 | 93 +++++-------------- .../src/tests/tutorial/templates/layout.jinja2 | 64 +++++++++++++ .../src/tests/tutorial/templates/login.jinja2 | 100 ++++++--------------- .../wiki2/src/tests/tutorial/templates/view.jinja2 | 89 ++++-------------- .../src/tests/tutorial/tests/test_functional.py | 56 +++++++----- .../wiki2/src/tests/tutorial/tests/test_views.py | 66 ++++++++------ .../wiki2/src/tests/tutorial/views/auth.py | 25 +++--- .../wiki2/src/tests/tutorial/views/default.py | 54 +++++------ docs/tutorials/wiki2/tests.rst | 43 +++------ 18 files changed, 354 insertions(+), 369 deletions(-) create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security/default.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 (limited to 'docs/tutorials') diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index 99c4ff0fe..f3079727e 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -17,6 +17,8 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +auth.secret = seekrit + # By default, the toolbar only appears for clients from IP addresses # '127.0.0.1' and '::1'. # debugtoolbar.hosts = 127.0.0.1 ::1 diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index cb1db3211..686dba48a 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -14,6 +14,8 @@ pyramid.default_locale_name = en sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +auth.secret = real-seekrit + [server:main] use = egg:waitress#main host = 0.0.0.0 diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index e06aa06e4..57538f2d0 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -43,8 +43,8 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', - install_requires=requires, tests_require=tests_require, + install_requires=requires, entry_points="""\ [paste.app_factory] main = tutorial:main diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py index a62c42378..f5c033b8b 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -1,28 +1,13 @@ from pyramid.config import Configurator -from pyramid.authentication import AuthTktAuthenticationPolicy -from pyramid.authorization import ACLAuthorizationPolicy - -from .security.default import groupfinder def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - authn_policy = AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder, hashalg='sha512') - authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.set_root_factory('.models.mymodel.RootFactory') - config.set_authentication_policy(authn_policy) - config.set_authorization_policy(authz_policy) - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('view_wiki', '/') - config.add_route('login', '/login') - config.add_route('logout', '/logout') - config.add_route('view_page', '/{pagename}') - config.add_route('add_page', '/add_page/{pagename}') - config.add_route('edit_page', '/{pagename}/edit_page') + config.include('.routes') + config.include('.security') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py index 25b0a8187..6fb32a1b2 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py @@ -18,10 +18,12 @@ class User(Base): password_hash = Column(Text) def set_password(self, pw): - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): if self.password_hash is not None: - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py new file mode 100644 index 000000000..c7c3a2120 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py @@ -0,0 +1,50 @@ +from pyramid.httpexceptions import HTTPNotFound +from pyramid.security import ( + Allow, + Everyone, +) + +from .models import Page + +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}', factory=page_factory) + config.add_route('add_page', '/add_page/{pagename}', + factory=new_page_factory) + config.add_route('edit_page', '/{pagename}/edit_page', + factory=page_factory) + +def new_page_factory(request): + pagename = request.matchdict['pagename'] + return NewPage(pagename) + +class NewPage(object): + def __init__(self, pagename): + self.pagename = pagename + + def __acl__(self): + return [ + (Allow, 'role:editor', 'create'), + (Allow, 'role:basic', 'create'), + ] + +def page_factory(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound + return PageResource(page) + +class PageResource(object): + def __init__(self, page): + self.page = page + + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, 'role:editor', 'edit'), + (Allow, str(self.page.creator_id), 'edit'), + ] diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py new file mode 100644 index 000000000..25cff7b05 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py @@ -0,0 +1,40 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.security import ( + Authenticated, + Everyone, +) + +from .models import User + + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + user = request.user + if user is not None: + return user.id + + def effective_principals(self, request): + principals = [Everyone] + user = request.user + if user is not None: + principals.append(Authenticated) + principals.append(str(user.id)) + principals.append('role:' + user.role) + return principals + +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user + +def includeme(config): + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py deleted file mode 100644 index 7fc1ea7c8..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py +++ /dev/null @@ -1,12 +0,0 @@ -USERS = { - 'editor': 'editor', - 'viewer': 'viewer', -} - -GROUPS = { - 'editor': ['group:editors'], -} - -def groupfinder(userid, request): - if userid in USERS: - return GROUPS.get(userid, []) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 index 4d767cfbe..7db25c674 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -1,73 +1,20 @@ - - - - - - - - - - - Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -

-
-
-
- -
-
-
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} -

- Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

- -
- -
-
- -
- -
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} + +{% block content %} +

+Editing {{pagename}} +

+

You can return to the +FrontPage. +

+
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..44d14304e --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 @@ -0,0 +1,64 @@ + + + + + + + + + + + {% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if request.user is none %} +

+ Login +

+ {% else %} +

+ {{request.user.name}} Logout +

+ {% endif %} + {% block content %}{% endblock %} +
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 index a80a2a165..1806de0ff 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 @@ -1,74 +1,26 @@ - - - - - - - - - - - Login - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- - Login -
- {{ message }} -

-
- -
- - -
-
- - -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +

+ + Login +
+{{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 index 942b8479b..94419e228 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -1,71 +1,18 @@ - - - - - - - - - - - {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} -

{{ content|safe }}

-

- - Edit this page - -

-

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + +{% block content %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {{page.name}}, created by {{page.creator.name}}. +

+

You can return to the +FrontPage. +

+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index c716537ae..b2c6e0975 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -5,26 +5,30 @@ from webtest import TestApp class FunctionalTests(unittest.TestCase): - viewer_login = ( - '/login?login=viewer&password=viewer' - '&came_from=FrontPage&form.submitted=Login') - viewer_wrong_login = ( - '/login?login=viewer&password=incorrect' - '&came_from=FrontPage&form.submitted=Login') + basic_login = ( + '/login?login=basic&password=basic' + '&next=FrontPage&form.submitted=Login') + basic_wrong_login = ( + '/login?login=basic&password=incorrect' + '&next=FrontPage&form.submitted=Login') editor_login = ( '/login?login=editor&password=editor' - '&came_from=FrontPage&form.submitted=Login') + '&next=FrontPage&form.submitted=Login') @classmethod def setUpClass(cls): from tutorial.models.meta import Base from tutorial.models import ( + User, Page, get_tm_session, ) from tutorial import main - settings = {'sqlalchemy.url': 'sqlite://'} + settings = { + 'sqlalchemy.url': 'sqlite://', + 'auth.secret': 'seekrit', + } app = main({}, **settings) cls.testapp = TestApp(app) @@ -34,8 +38,15 @@ class FunctionalTests(unittest.TestCase): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + editor = User(name='editor', role='editor') + editor.set_password('editor') + basic = User(name='basic', role='basic') + basic.set_password('basic') + page1 = Page(name='FrontPage', data='This is the front page') + page1.creator = editor + page2 = Page(name='BackPage', data='This is the back page') + page2.creator = basic + dbsession.add_all([basic, editor, page1, page2]) @classmethod def tearDownClass(cls): @@ -54,20 +65,20 @@ class FunctionalTests(unittest.TestCase): self.testapp.get('/SomePage', status=404) def test_successful_log_in(self): - res = self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get(self.basic_login, status=302) self.assertEqual(res.location, 'http://localhost/FrontPage') def test_failed_log_in(self): - res = self.testapp.get(self.viewer_wrong_login, status=200) + res = self.testapp.get(self.basic_wrong_login, status=200) self.assertTrue(b'login' in res.body) def test_logout_link_present_when_logged_in(self): - self.testapp.get(self.viewer_login, status=302) + self.testapp.get(self.basic_login, status=302) res = self.testapp.get('/FrontPage', status=200) self.assertTrue(b'Logout' in res.body) def test_logout_link_not_present_after_logged_out(self): - self.testapp.get(self.viewer_login, status=302) + self.testapp.get(self.basic_login, status=302) self.testapp.get('/FrontPage', status=200) res = self.testapp.get('/logout', status=302) self.assertTrue(b'Logout' not in res.body) @@ -80,15 +91,20 @@ class FunctionalTests(unittest.TestCase): res = self.testapp.get('/add_page/NewPage', status=302).follow() self.assertTrue(b'Login' in res.body) - def test_viewer_user_cannot_edit(self): - self.testapp.get(self.viewer_login, status=302) + def test_basic_user_cannot_edit_front(self): + self.testapp.get(self.basic_login, status=302) res = self.testapp.get('/FrontPage/edit_page', status=302).follow() self.assertTrue(b'Login' in res.body) - def test_viewer_user_cannot_add(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=302).follow() - self.assertTrue(b'Login' in res.body) + def test_basic_user_can_edit_back(self): + self.testapp.get(self.basic_login, status=302) + res = self.testapp.get('/BackPage/edit_page', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_basic_user_can_add(self): + self.testapp.get(self.basic_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Editing' in res.body) def test_editors_member_user_can_edit(self): self.testapp.get(self.editor_login, status=302) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py index b2830d070..5253183df 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -8,12 +8,6 @@ def dummy_request(dbsession): return testing.DummyRequest(dbsession=dbsession) -def _register_routes(config): - config.add_route('view_page', '{pagename}') - config.add_route('add_page', 'add_page/{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - - class BaseTest(unittest.TestCase): def setUp(self): from ..models import get_tm_session @@ -21,7 +15,7 @@ class BaseTest(unittest.TestCase): 'sqlalchemy.url': 'sqlite:///:memory:' }) self.config.include('..models') - self.config.include(_register_routes) + self.config.include('..routes') session_factory = self.config.registry['dbsession_factory'] self.session = get_tm_session(session_factory, transaction.manager) @@ -38,11 +32,21 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() + def makeUser(self, name, role, password='dummy'): + from ..models import User + user = User(name=name, role=role) + user.set_password(password) + return user + + def makePage(self, name, data, creator): + from ..models import Page + return Page(name=name, data=data, creator=creator) + class ViewWikiTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() - _register_routes(self.config) + self.config.include('..routes') def tearDown(self): testing.tearDown() @@ -63,14 +67,16 @@ class ViewPageTests(BaseTest): return view_page(request) def test_it(self): + from ..routes import PageResource + # add a page to the db - from ..models.mymodel import Page - page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') - self.session.add(page) + user = self.makeUser('foo', 'editor') + page = self.makePage('IDoExist', 'Hello CruelWorld IDoExist', user) + self.session.add_all([page, user]) # create a request asking for the page we've created request = dummy_request(self.session) - request.matchdict['pagename'] = 'IDoExist' + request.context = PageResource(page) # call the view we're testing and check its behavior info = self._callFUT(request) @@ -93,19 +99,23 @@ class AddPageTests(BaseTest): return add_page(request) def test_it_notsubmitted(self): + from ..routes import NewPage request = dummy_request(self.session) - request.matchdict = {'pagename': 'AnotherPage'} + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') info = self._callFUT(request) - self.assertEqual(info['page'].data, '') + self.assertEqual(info['pagedata'], '') self.assertEqual(info['save_url'], 'http://example.com/add_page/AnotherPage') def test_it_submitted(self): - from ..models.mymodel import Page + from ..models import Page + from ..routes import NewPage request = testing.DummyRequest({'form.submitted': True, 'body': 'Hello yo!'}, dbsession=self.session) - request.matchdict = {'pagename': 'AnotherPage'} + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') self._callFUT(request) page = self.session.query(Page).filter_by(name='AnotherPage').one() self.assertEqual(page.data, 'Hello yo!') @@ -116,25 +126,31 @@ class EditPageTests(BaseTest): from tutorial.views.default import edit_page return edit_page(request) + def makeContext(self, page): + from ..routes import PageResource + return PageResource(page) + def test_it_notsubmitted(self): - from ..models.mymodel import Page + user = self.makeUser('foo', 'editor') + page = self.makePage('abc', 'hello', user) + self.session.add_all([page, user]) + request = dummy_request(self.session) - request.matchdict = {'pagename': 'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) + request.context = self.makeContext(page) info = self._callFUT(request) - self.assertEqual(info['page'], page) + self.assertEqual(info['pagename'], 'abc') self.assertEqual(info['save_url'], 'http://example.com/abc/edit_page') def test_it_submitted(self): - from ..models.mymodel import Page + user = self.makeUser('foo', 'editor') + page = self.makePage('abc', 'hello', user) + self.session.add_all([page, user]) + request = testing.DummyRequest({'form.submitted': True, 'body': 'Hello yo!'}, dbsession=self.session) - request.matchdict = {'pagename': 'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) + request.context = self.makeContext(page) response = self._callFUT(request) self.assertEqual(response.location, 'http://example.com/abc') self.assertEqual(page.data, 'Hello yo!') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py index 08aa2bfad..2b993b430 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py @@ -8,33 +8,30 @@ from pyramid.view import ( view_config, ) -from ..security.default import USERS +from ..models import User -@view_config(route_name='login', renderer='templates/login.jinja2') +@view_config(route_name='login', renderer='../templates/login.jinja2') def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) + next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') message = '' login = '' - password = '' if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, headers=headers) + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) message = 'Failed login' return dict( message=message, url=request.route_url('login'), - came_from=came_from, + next_url=next_url, login=login, - password=password, ) @view_config(route_name='logout') @@ -45,5 +42,5 @@ def logout(request): @forbidden_view_config() def forbidden_view(request): - next_url = request.route_url('login', _query={'came_from': request.url}) + next_url = request.route_url('login', _query={'next': request.url}) return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index 6fb3c8744..9358993ea 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -2,10 +2,7 @@ import cgi import re from docutils.core import publish_parts -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) +from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from ..models import Page @@ -13,7 +10,7 @@ from ..models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki', permission='view') +@view_config(route_name='view_wiki') def view_wiki(request): next_url = request.route_url('view_page', pagename='FrontPage') return HTTPFound(location=next_url) @@ -21,12 +18,9 @@ def view_wiki(request): @view_config(route_name='view_page', renderer='../templates/view.jinja2', permission='view') def view_page(request): - pagename = request.matchdict['pagename'] - page = request.dbsession.query(Page).filter_by(name=pagename).first() - if page is None: - return HTTPNotFound('No such page') + page = request.context.page - def check(match): + def add_link(match): word = match.group(1) exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: @@ -37,34 +31,34 @@ def view_page(request): return '%s' % (add_url, cgi.escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=page.name) return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='../templates/edit.jinja2', +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', permission='edit') +def edit_page(request): + page = request.context.page + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=page.name) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=page.name), + ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2', + permission='create') def add_page(request): - pagename = request.matchdict['pagename'] + pagename = request.context.pagename if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) + page.creator = request.user request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url) - -@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', - permission='edit') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = request.dbsession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - next_url = request.route_url('view_page', pagename=pagename) - return HTTPFound(location=next_url) - return dict( - page=page, - save_url=request.route_url('edit_page', pagename=pagename), - ) + return dict(pagename=pagename, pagedata='', save_url=save_url) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index a99cd68cc..667550467 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -43,7 +43,7 @@ Functional tests We'll test the whole application, covering security aspects that are not tested in the unit tests, like logging in, logging out, checking that -the ``viewer`` user cannot add or edit pages, but the ``editor`` user +the ``basic`` user cannot edit pages it didn't create, but the ``editor`` user can, and so on. @@ -65,39 +65,20 @@ follows: :language: python -Running the tests -================= - -We can run these tests by using ``setup.py test`` in the same way we did in -:ref:`running_tests`. However, first we must edit our ``setup.py`` to include -a dependency on `WebTest -`_, which we've used -in our ``tests.py``. Change the ``requires`` list in ``setup.py`` to include -``WebTest``. - -.. literalinclude:: src/tests/setup.py - :linenos: - :language: python - :lines: 11-22 - :emphasize-lines: 11 +.. note:: -After we've added a dependency on WebTest in ``setup.py``, we need to run -``setup.py develop`` to get WebTest installed into our virtualenv. Assuming -our shell's current working directory is the "tutorial" distribution directory: + We're utilizing the excellent WebTest_ package to do functional testing + of the application. This is defined in the ``tests_require`` section of + our ``setup.py``. Any other dependencies needed only for testing purposes + can be added there and will be installed automatically when running + ``setup.py test``. -On UNIX: - -.. code-block:: bash - $ $VENV/bin/python setup.py develop - -On Windows: - -.. code-block:: text - - c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop +Running the tests +================= -Once that command has completed successfully, we can run the tests themselves: +We can run these tests by using ``setup.py test`` in the same way we did in +:ref:`running_tests`: On UNIX: @@ -122,3 +103,5 @@ The expected result should look like the following: OK Process finished with exit code 0 + +.. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/ -- cgit v1.2.3