From 406024de0d1792cbca5056ceaa94ff19654f730e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 11 Nov 2015 21:28:28 -0800 Subject: basiclayout/tutorial/setup.py - update template binding from chameleon to jinja2 --- docs/tutorials/wiki2/src/basiclayout/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index 15e7e5923..eb771010f 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'pyramid_tm', 'SQLAlchemy', -- cgit v1.2.3 From 392cc17f5091d51a8e1b8908f8600c0b821ebd7f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 11 Nov 2015 21:58:03 -0800 Subject: basiclayout/tutorial - views - use package instead of single file - add tests.py from scaffold - update basiclayout.rst with views section --- docs/tutorials/wiki2/basiclayout.rst | 32 +++++----- .../wiki2/src/basiclayout/tutorial/tests.py | 68 ++++++++++++++++------ .../wiki2/src/basiclayout/tutorial/views.py | 35 ----------- .../src/basiclayout/tutorial/views/__init__.py | 0 .../src/basiclayout/tutorial/views/default.py | 33 +++++++++++ 5 files changed, 100 insertions(+), 68 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views.py create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 8a1dbf3f8..cdcf0fa5f 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -116,17 +116,19 @@ Finally ``main`` is finished configuring things, so it uses the :lines: 21 :language: py -View declarations via ``views.py`` ----------------------------------- + +View declarations via the ``views`` package +------------------------------------------- The main function of a web framework is mapping each URL pattern to code (a :term:`view callable`) that is executed when the requested URL matches the corresponding :term:`route`. Our application uses the :meth:`pyramid.view.view_config` decorator to perform this mapping. -Open ``tutorial/tutorial/views.py``. It should already contain the following: +Open ``tutorial/tutorial/views/default.py`` in the ``views`` package. It +should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/views.py + .. literalinclude:: src/basiclayout/tutorial/views/default.py :linenos: :language: py @@ -135,13 +137,13 @@ function it decorates (``my_view``) with a :term:`view configuration`, consisting of: * a ``route_name`` (``home``) - * a ``renderer``, which is a template from the ``templates`` subdirectory - of the package. + * a ``renderer``, which is a template from the ``templates`` subdirectory of + the package. When the pattern associated with the ``home`` view is matched during a request, -``my_view()`` will be executed. ``my_view()`` returns a dictionary; the -renderer will use the ``templates/mytemplate.pt`` template to create a response -based on the values in the dictionary. +``my_view()`` will be executed. ``my_view()`` returns a dictionary; the +renderer will use the ``templates/mytemplate.jinja2`` template to create a +response based on the values in the dictionary. Note that ``my_view()`` accepts a single argument named ``request``. This is the standard call signature for a Pyramid :term:`view callable`. @@ -154,13 +156,13 @@ application. Without being processed by ``scan``, the decorator effectively does nothing. ``@view_config`` is inert without being detected via a :term:`scan`. -The sample ``my_view()`` created by the scaffold uses a ``try:`` and ``except:`` -clause to detect if there is a problem accessing the project database and -provide an alternate error response. That response will include the text -shown at the end of the file, which will be displayed in the browser to -inform the user about possible actions to take to solve the problem. +The sample ``my_view()`` created by the scaffold uses a ``try:`` and +``except:`` clause to detect if there is a problem accessing the project +database and provide an alternate error response. That response will include +the text shown at the end of the file, which will be displayed in the browser +to inform the user about possible actions to take to solve the problem. -Content Models with ``models.py`` +Content models with ``models.py`` --------------------------------- .. START moved from Application configuration with ``__init__.py``. This diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py index 57a775e0a..b947e3bb1 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py @@ -3,31 +3,63 @@ import transaction from pyramid import testing -from .models import DBSession +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) -class TestMyView(unittest.TestCase): + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - from .models import ( - Base, - MyModel, + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() + + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, ) - DBSession.configure(bind=engine) - Base.metadata.create_all(engine) - with transaction.manager: - model = MyModel(name='one', value=55) - DBSession.add(model) + + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) + + self.session = get_session(transaction.manager, dbmaker) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - DBSession.remove() + from .models.meta import Base + testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def test_it(self): - from .views import my_view - request = testing.DummyRequest() - info = my_view(request) + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models.mymodel import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) self.assertEqual(info['one'].name, 'one') self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py deleted file mode 100644 index 4cfcae4af..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py +++ /dev/null @@ -1,35 +0,0 @@ -from pyramid.response import Response -from pyramid.view import view_config - -from sqlalchemy.exc import DBAPIError - -from .models import ( - DBSession, - MyModel, - ) - - -@view_config(route_name='home', renderer='templates/mytemplate.pt') -def my_view(request): - try: - one = DBSession.query(MyModel).filter(MyModel.name == 'one').first() - except DBAPIError: - return Response(conn_err_msg, content_type='text/plain', status_int=500) - return {'one': one, 'project': 'tutorial'} - -conn_err_msg = """\ -Pyramid is having a problem using your SQL database. The problem -might be caused by one of the following things: - -1. You may need to run the "initialize_tutorial_db" script - to initialize your database tables. Check your virtual - environment's "bin" directory for this script and try to run it. - -2. Your database server may not be running. Check that the - database server referred to by the "sqlalchemy.url" setting in - your "development.ini" file is running. - -After you fix the problem, please restart the Pyramid application to -try it again. -""" - diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py new file mode 100644 index 000000000..13ad8793c --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py @@ -0,0 +1,33 @@ +from pyramid.response import Response +from pyramid.view import view_config + +from sqlalchemy.exc import DBAPIError + +from ..models.mymodel import MyModel + + +@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') +def my_view(request): + try: + query = request.dbsession.query(MyModel) + one = query.filter(MyModel.name == 'one').first() + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status_int=500) + return {'one': one, 'project': 'tutorial'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_tutorial_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" -- cgit v1.2.3 From 0409cf178d4c0fa9a0d3b0858ab2294935b23609 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 12 Nov 2015 00:40:02 -0800 Subject: basiclayout/tutorial - models, scripts, static, templates - use package instead of single file - add tests.py from scaffold - update basiclayout.rst with models section --- docs/tutorials/wiki2/basiclayout.rst | 117 +++++++++++---------- .../wiki2/src/basiclayout/tutorial/models.py | 27 ----- .../src/basiclayout/tutorial/models/__init__.py | 7 ++ .../wiki2/src/basiclayout/tutorial/models/meta.py | 46 ++++++++ .../src/basiclayout/tutorial/models/mymodel.py | 17 +++ .../basiclayout/tutorial/scripts/initializedb.py | 30 ++++-- .../src/basiclayout/tutorial/static/theme.min.css | 2 +- .../basiclayout/tutorial/templates/layout.jinja2 | 66 ++++++++++++ .../tutorial/templates/mytemplate.jinja2 | 8 ++ .../basiclayout/tutorial/templates/mytemplate.pt | 66 ------------ 10 files changed, 225 insertions(+), 161 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/models.py create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 delete mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index cdcf0fa5f..5d9b62eea 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -113,7 +113,7 @@ Finally ``main`` is finished configuring things, so it uses the :term:`WSGI` application: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 21 + :lines: 13 :language: py @@ -162,94 +162,99 @@ database and provide an alternate error response. That response will include the text shown at the end of the file, which will be displayed in the browser to inform the user about possible actions to take to solve the problem. -Content models with ``models.py`` ---------------------------------- +Content models with the ``models`` package +------------------------------------------ -.. START moved from Application configuration with ``__init__.py``. This - section is a WIP, and needs to be updated using the new models package. +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. -The main function first creates a :term:`SQLAlchemy` database engine using -:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.`` prefixed -settings in the ``development.ini`` file's ``[app:main]`` section. -This will be a URI (something like ``sqlite://``): +First, open ``tutorial/tutorial/models/__init__.py``, which should already +contain the following: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 + .. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :linenos: :language: py -``main`` then initializes our SQLAlchemy session object, passing it the -engine: +Our ``__init__.py`` will perform some imports to support later code, then calls +the function :func:`sqlalchemy.orm.configure_mappers`. - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 14 +Next open ``tutorial/tutorial/models/meta.py``, which should already contain +the following: + + .. literalinclude:: src/basiclayout/tutorial/models/meta.py + :linenos: :language: py -``main`` subsequently initializes our SQLAlchemy declarative ``Base`` object, -assigning the engine we created to the ``bind`` attribute of it's -``metadata`` object. This allows table definitions done imperatively -(instead of declaratively, via a class statement) to work. We won't use any -such tables in our application, but if you add one later, long after you've -forgotten about this tutorial, you won't be left scratching your head when it -doesn't work. +``meta.py`` contains imports that are used to support later code. We create a +dictionary ``NAMING_CONVENTION`` as well. - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 15 + .. literalinclude:: src/basiclayout/tutorial/models/meta.py + :end-before: metadata + :linenos: :language: py -.. END moved from Application configuration with ``__init__.py`` +Next we create a ``metadata`` object from the class +:class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value +for the ``naming_convention`` argument. 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. -In a SQLAlchemy-based application, a *model* object is an object composed by -querying the SQL database. The ``models.py`` file is where the ``alchemy`` -scaffold put the classes that implement our models. + .. literalinclude:: src/basiclayout/tutorial/models/meta.py + :lines: 15-16 + :lineno-start: 15 + :linenos: + :language: py -Open ``tutorial/tutorial/models.py``. It should already contain the following: +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.py + .. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: includeme :linenos: :language: py -Let's examine this in detail. First, we need some imports to support later code: +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.py - :end-before: DBSession + .. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_session :linenos: :language: py -Next we set up a SQLAlchemy ``DBSession`` object: +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.py - :lines: 17 + .. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_engine + :linenos: :language: py -``scoped_session`` and ``sessionmaker`` are standard SQLAlchemy helpers. -``scoped_session`` allows us to access our database connection globally. -``sessionmaker`` creates a database session object. We pass to -``sessionmaker`` the ``extension=ZopeTransactionExtension()`` extension -option in order to allow the system to automatically manage database -transactions. With ``ZopeTransactionExtension`` activated, our application -will automatically issue a transaction commit after every request unless an -exception is raised, in which case the transaction will be aborted. - -We also need to create a declarative ``Base`` object to use as a -base class for our model: +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.py - :lines: 17 + .. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_dbmaker + :linenos: :language: py -Our model classes will inherit from this ``Base`` class so they can be -associated with our particular database connection. - -To give a simple example of a model class, we define one named ``MyModel``: +To give a simple example of a model class, we define one named ``MyModel``: - .. literalinclude:: src/basiclayout/tutorial/models.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 accepts keyword arguments of the same name as that of the mapped attributes. +supplies for us a default constructor if one is not already present, which +accepts keyword arguments of the same name as that of the mapped attributes. .. note:: Example usage of MyModel: diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py deleted file mode 100644 index 11ddccadb..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py +++ /dev/null @@ -1,27 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, - Index, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class MyModel(Base): - __tablename__ = 'models' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - value = Column(Integer) - -Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py new file mode 100644 index 000000000..6ffc10a78 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import MyModel # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py @@ -0,0 +1,46 @@ +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 + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py new file mode 100644 index 000000000..5a2b5890c --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py @@ -0,0 +1,17 @@ +from .meta import Base +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py index 66feb3008..f0d09729e 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py @@ -2,36 +2,44 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, ) -from ..models import ( - DBSession, - MyModel, +from pyramid.scripts.common import parse_vars + +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import MyModel def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s \n' + print('usage: %s [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = MyModel(name='one', value=1) - DBSession.add(model) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css index 2f924bcc5..0d25de5b6 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.min.css @@ -1 +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{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file +@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/basiclayout/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ff624c65b --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% 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/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt deleted file mode 100644 index c9b0cec21..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

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

-
-
-
- -
- -
-
-
- - - - - - - - -- cgit v1.2.3 From 1b994c6b87449a9542dcf5f0daf44023dbff31ff Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 12 Nov 2015 01:04:18 -0800 Subject: basiclayout/tutorial poke in attempt to resolve merge conflict on GitHub --- docs/tutorials/wiki2/basiclayout.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 5d9b62eea..c293f3170 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -162,6 +162,7 @@ database and provide an alternate error response. That response will include the text shown at the end of the file, which will be displayed in the browser to inform the user about possible actions to take to solve the problem. + Content models with the ``models`` package ------------------------------------------ -- cgit v1.2.3 From bfa4994ab0d1327c451509f466ccebbad8fa2320 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 12 Nov 2015 03:02:11 -0800 Subject: update wiki2/src/models and wiki2/definingmodels.rst --- docs/tutorials/wiki2/definingmodels.rst | 49 +++++++++++----- docs/tutorials/wiki2/src/models/development.ini | 4 +- docs/tutorials/wiki2/src/models/production.ini | 14 +++-- docs/tutorials/wiki2/src/models/setup.py | 2 +- .../wiki2/src/models/tutorial/__init__.py | 12 +--- docs/tutorials/wiki2/src/models/tutorial/models.py | 25 -------- .../wiki2/src/models/tutorial/models/__init__.py | 7 +++ .../wiki2/src/models/tutorial/models/meta.py | 46 +++++++++++++++ .../wiki2/src/models/tutorial/models/mymodel.py | 14 +++++ .../src/models/tutorial/scripts/initializedb.py | 25 ++++---- .../wiki2/src/models/tutorial/static/theme.min.css | 2 +- .../src/models/tutorial/templates/layout.jinja2 | 66 +++++++++++++++++++++ .../models/tutorial/templates/mytemplate.jinja2 | 8 +++ .../src/models/tutorial/templates/mytemplate.pt | 66 --------------------- docs/tutorials/wiki2/src/models/tutorial/tests.py | 68 ++++++++++++++++------ docs/tutorials/wiki2/src/models/tutorial/views.py | 35 ----------- .../wiki2/src/models/tutorial/views/__init__.py | 0 .../wiki2/src/models/tutorial/views/default.py | 33 +++++++++++ 18 files changed, 289 insertions(+), 187 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/models/tutorial/models.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 delete mode 100644 docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt delete mode 100644 docs/tutorials/wiki2/src/models/tutorial/views.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/views/default.py diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index b2d9bf83a..b38177d04 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -4,27 +4,27 @@ Defining the Domain Model The first change we'll make to our stock ``pcreate``-generated application will be to define a :term:`domain model` constructor representing a wiki page. -We'll do this inside our ``models.py`` file. +We'll do this inside our ``mymodel.py`` file. -Edit ``models.py`` ------------------- +Edit ``mymodel.py`` +------------------- .. note:: - There is nothing special about the filename ``models.py``. A + 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``, but this is only by convention. + ``models`` (as we've done in this tutorial), but this is only by convention. -Open ``tutorial/tutorial/models.py`` file and edit it to look like the -following: +Open the ``tutorial/tutorial/models/mymodel.py`` file and edit it to look like +the following: -.. literalinclude:: src/models/tutorial/models.py +.. literalinclude:: src/models/tutorial/models/mymodel.py :linenos: :language: py - :emphasize-lines: 20-22,24,25 + :emphasize-lines: 9-11,13,14 The highlighted lines are the ones that need to be changed, as well as removing lines that reference ``Index``. @@ -33,11 +33,11 @@ The first thing we've done is remove the stock ``MyModel`` class from the generated ``models.py`` file. The ``MyModel`` class is only a sample and we're not going to use it. -Then, we added a ``Page`` class. Because this is a SQLAlchemy application, +Then we added a ``Page`` class. Because this is an SQLAlchemy application, this class inherits from an instance of :func:`sqlalchemy.ext.declarative.declarative_base`. -.. literalinclude:: src/models/tutorial/models.py +.. literalinclude:: src/models/tutorial/models/mymodel.py :pyobject: Page :linenos: :language: python @@ -45,15 +45,33 @@ this class inherits from an instance of 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`` +``Page`` class will also have class-level attributes named ``id``, ``name``, and ``data`` (all instances of :class:`sqlalchemy.schema.Column`). These will map to columns in the ``pages`` table. The ``id`` attribute will be the primary key in the table. The ``name`` attribute will be a text attribute, each value of which needs to be unique within the column. The ``data`` attribute is a text attribute that will hold the body of each page. -Changing ``scripts/initializedb.py`` ------------------------------------- + +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 +the following: + +.. literalinclude:: src/models/tutorial/models/__init__.py + :linenos: + :language: py + :emphasize-lines: 4 + +Here we need to align our import with the name of the model ``Page``. + + +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``. @@ -72,12 +90,13 @@ the following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: :language: python - :emphasize-lines: 14,31,36 + :emphasize-lines: 16,31,41 Only the highlighted lines need to be changed, as well as removing the lines referencing ``pyramid.scripts.common`` and ``options`` under the ``main`` function. + Installing the project and re-initializing the database ------------------------------------------------------- diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini index a9d53b296..99c4ff0fe 100644 --- a/docs/tutorials/wiki2/src/models/development.ini +++ b/docs/tutorials/wiki2/src/models/development.ini @@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini index 4684d2f7a..97acfbd7d 100644 --- a/docs/tutorials/wiki2/src/models/production.ini +++ b/docs/tutorials/wiki2/src/models/production.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -16,7 +21,10 @@ use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -51,6 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index 15e7e5923..eb771010f 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'pyramid_tm', 'SQLAlchemy', diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py index 867049e4f..7994bbfa8 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py @@ -1,20 +1,12 @@ from pyramid.config import Configurator -from sqlalchemy import engine_from_config - -from .models import ( - DBSession, - Base, - ) def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine config = Configurator(settings=settings) - config.include('pyramid_chameleon') + config.include('pyramid_jinja2') + config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/docs/tutorials/wiki2/src/models/tutorial/models.py b/docs/tutorials/wiki2/src/models/tutorial/models.py deleted file mode 100644 index f028c917a..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/models.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py new file mode 100644 index 000000000..7b1c62867 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import Page # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py @@ -0,0 +1,46 @@ +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 + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py new file mode 100644 index 000000000..45571d78e --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py @@ -0,0 +1,14 @@ +from .meta import Base +from sqlalchemy import ( + Column, + Integer, + Text, +) + + +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/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py index 23a5f13f4..4aac4a848 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -2,36 +2,41 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, ) -from ..models import ( - DBSession, - Page, +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import Page def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s \n' + print('usage: %s [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] setup_logging(config_uri) settings = get_appsettings(config_uri) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css index 2f924bcc5..0d25de5b6 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css +++ b/docs/tutorials/wiki2/src/models/tutorial/static/theme.min.css @@ -1 +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{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file +@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/models/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ff624c65b --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% 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/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt deleted file mode 100644 index c9b0cec21..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

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

-
-
-
- -
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py index 57a775e0a..b947e3bb1 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py @@ -3,31 +3,63 @@ import transaction from pyramid import testing -from .models import DBSession +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) -class TestMyView(unittest.TestCase): + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - from .models import ( - Base, - MyModel, + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() + + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, ) - DBSession.configure(bind=engine) - Base.metadata.create_all(engine) - with transaction.manager: - model = MyModel(name='one', value=55) - DBSession.add(model) + + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) + + self.session = get_session(transaction.manager, dbmaker) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - DBSession.remove() + from .models.meta import Base + testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def test_it(self): - from .views import my_view - request = testing.DummyRequest() - info = my_view(request) + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models.mymodel import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) self.assertEqual(info['one'].name, 'one') self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/models/tutorial/views.py b/docs/tutorials/wiki2/src/models/tutorial/views.py deleted file mode 100644 index 4cfcae4af..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/views.py +++ /dev/null @@ -1,35 +0,0 @@ -from pyramid.response import Response -from pyramid.view import view_config - -from sqlalchemy.exc import DBAPIError - -from .models import ( - DBSession, - MyModel, - ) - - -@view_config(route_name='home', renderer='templates/mytemplate.pt') -def my_view(request): - try: - one = DBSession.query(MyModel).filter(MyModel.name == 'one').first() - except DBAPIError: - return Response(conn_err_msg, content_type='text/plain', status_int=500) - return {'one': one, 'project': 'tutorial'} - -conn_err_msg = """\ -Pyramid is having a problem using your SQL database. The problem -might be caused by one of the following things: - -1. You may need to run the "initialize_tutorial_db" script - to initialize your database tables. Check your virtual - environment's "bin" directory for this script and try to run it. - -2. Your database server may not be running. Check that the - database server referred to by the "sqlalchemy.url" setting in - your "development.ini" file is running. - -After you fix the problem, please restart the Pyramid application to -try it again. -""" - diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py new file mode 100644 index 000000000..13ad8793c --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py @@ -0,0 +1,33 @@ +from pyramid.response import Response +from pyramid.view import view_config + +from sqlalchemy.exc import DBAPIError + +from ..models.mymodel import MyModel + + +@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') +def my_view(request): + try: + query = request.dbsession.query(MyModel) + one = query.filter(MyModel.name == 'one').first() + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status_int=500) + return {'one': one, 'project': 'tutorial'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_tutorial_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" -- cgit v1.2.3 From 414b67b45bab156b9738105e180908c4eee8600a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 12 Nov 2015 12:56:27 -0600 Subject: Restore progress after backing changes out of master. This reverts commit 049e670aef9ea5611561546fd5c0e2dd6152b9b7. --- CONTRIBUTORS.txt | 2 + docs/quick_tour.rst | 4 +- docs/quick_tour/sqla_demo/development.ini | 4 +- docs/quick_tour/sqla_demo/production.ini | 2 +- docs/quick_tour/sqla_demo/setup.py | 11 +- docs/quick_tour/sqla_demo/sqla_demo.sqlite | Bin 3072 -> 0 bytes docs/quick_tour/sqla_demo/sqla_demo/__init__.py | 11 +- docs/quick_tour/sqla_demo/sqla_demo/models.py | 29 -- .../sqla_demo/sqla_demo/models/__init__.py | 7 + docs/quick_tour/sqla_demo/sqla_demo/models/meta.py | 46 +++ .../sqla_demo/sqla_demo/models/mymodel.py | 19 ++ .../sqla_demo/sqla_demo/scripts/initializedb.py | 30 +- .../sqla_demo/sqla_demo/static/favicon.ico | Bin 1406 -> 0 bytes .../sqla_demo/sqla_demo/static/footerbg.png | Bin 333 -> 0 bytes .../sqla_demo/sqla_demo/static/headerbg.png | Bin 203 -> 0 bytes docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css | 8 - .../sqla_demo/sqla_demo/static/middlebg.png | Bin 2797 -> 0 bytes .../sqla_demo/sqla_demo/static/pylons.css | 372 --------------------- .../sqla_demo/sqla_demo/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../sqla_demo/sqla_demo/static/pyramid-small.png | Bin 7044 -> 0 bytes .../sqla_demo/sqla_demo/static/pyramid.png | Bin 33055 -> 12901 bytes .../sqla_demo/sqla_demo/static/theme.css | 154 +++++++++ .../sqla_demo/sqla_demo/static/theme.min.css | 1 + .../sqla_demo/sqla_demo/static/transparent.gif | Bin 49 -> 0 bytes .../sqla_demo/sqla_demo/templates/layout.jinja2 | 66 ++++ .../sqla_demo/templates/mytemplate.jinja2 | 8 + .../sqla_demo/sqla_demo/templates/mytemplate.pt | 76 ----- docs/quick_tour/sqla_demo/sqla_demo/tests.py | 68 +++- docs/quick_tour/sqla_demo/sqla_demo/views.py | 37 -- .../sqla_demo/sqla_demo/views/__init__.py | 0 .../sqla_demo/sqla_demo/views/default.py | 35 ++ docs/tutorials/wiki2/basiclayout.rst | 98 +++--- .../wiki2/src/basiclayout/development.ini | 4 +- .../tutorials/wiki2/src/basiclayout/production.ini | 2 +- .../wiki2/src/basiclayout/tutorial/__init__.py | 12 +- pyramid/scaffolds/alchemy/+package+/__init__.py | 12 +- pyramid/scaffolds/alchemy/+package+/models.py | 27 -- .../scaffolds/alchemy/+package+/models/__init__.py | 7 + pyramid/scaffolds/alchemy/+package+/models/meta.py | 46 +++ .../scaffolds/alchemy/+package+/models/mymodel.py | 17 + .../alchemy/+package+/scripts/initializedb.py | 21 +- .../alchemy/+package+/templates/layout.jinja2_tmpl | 66 ++++ .../+package+/templates/mytemplate.jinja2_tmpl | 8 + .../alchemy/+package+/templates/mytemplate.pt_tmpl | 67 ---- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 80 +++-- pyramid/scaffolds/alchemy/+package+/views.py_tmpl | 36 -- .../scaffolds/alchemy/+package+/views/__init__.py | 0 .../alchemy/+package+/views/default.py_tmpl | 33 ++ pyramid/scaffolds/alchemy/setup.py_tmpl | 2 +- 49 files changed, 719 insertions(+), 809 deletions(-) delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo.sqlite delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/models.py create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/models/meta.py create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/favicon.ico delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/footerbg.png delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/headerbg.png delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/middlebg.png delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/pylons.css create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-small.png create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/theme.css create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/transparent.gif create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2 create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2 delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo/views.py create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/views/default.py delete mode 100644 pyramid/scaffolds/alchemy/+package+/models.py create mode 100644 pyramid/scaffolds/alchemy/+package+/models/__init__.py create mode 100644 pyramid/scaffolds/alchemy/+package+/models/meta.py create mode 100644 pyramid/scaffolds/alchemy/+package+/models/mymodel.py create mode 100644 pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl create mode 100644 pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl delete mode 100644 pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl delete mode 100644 pyramid/scaffolds/alchemy/+package+/views.py_tmpl create mode 100644 pyramid/scaffolds/alchemy/+package+/views/__init__.py create mode 100644 pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 2ef07af75..0c79b9a41 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -247,6 +247,8 @@ Contributors - Donald Stufft, 2015/03/15 +- Randy Topliffe, 2015/04/14 + - Karen Dalton, 2015/06/01 - Igor Stroh, 2015/06/10 diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index be5be2e36..87c6f1e1c 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -818,14 +818,14 @@ The ORM eases the mapping of database structures into a programming language. SQLAlchemy uses "models" for this mapping. The scaffold generated a sample model: -.. literalinclude:: quick_tour/sqla_demo/sqla_demo/models.py +.. literalinclude:: quick_tour/sqla_demo/sqla_demo/models/mymodel.py :start-after: Start Sphinx Include :end-before: End Sphinx Include View code, which mediates the logic between web requests and the rest of the system, can then easily get at the data thanks to SQLAlchemy: -.. literalinclude:: quick_tour/sqla_demo/sqla_demo/views.py +.. literalinclude:: quick_tour/sqla_demo/sqla_demo/views/default.py :start-after: Start Sphinx Include :end-before: End Sphinx Include diff --git a/docs/quick_tour/sqla_demo/development.ini b/docs/quick_tour/sqla_demo/development.ini index 174468abf..0db0950a0 100644 --- a/docs/quick_tour/sqla_demo/development.ini +++ b/docs/quick_tour/sqla_demo/development.ini @@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/sqla_demo.sqlite [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/quick_tour/sqla_demo/production.ini b/docs/quick_tour/sqla_demo/production.ini index dc0ba304f..38f3b6318 100644 --- a/docs/quick_tour/sqla_demo/production.ini +++ b/docs/quick_tour/sqla_demo/production.ini @@ -59,4 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/quick_tour/sqla_demo/setup.py b/docs/quick_tour/sqla_demo/setup.py index ac2eed035..312a97c06 100644 --- a/docs/quick_tour/sqla_demo/setup.py +++ b/docs/quick_tour/sqla_demo/setup.py @@ -3,15 +3,18 @@ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'SQLAlchemy', 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', 'zope.sqlalchemy', 'waitress', ] diff --git a/docs/quick_tour/sqla_demo/sqla_demo.sqlite b/docs/quick_tour/sqla_demo/sqla_demo.sqlite deleted file mode 100644 index fa6adb104..000000000 Binary files a/docs/quick_tour/sqla_demo/sqla_demo.sqlite and /dev/null differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/__init__.py index aac7c5e69..7994bbfa8 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/__init__.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/__init__.py @@ -1,19 +1,12 @@ from pyramid.config import Configurator -from sqlalchemy import engine_from_config - -from .models import ( - DBSession, - Base, - ) def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models.py b/docs/quick_tour/sqla_demo/sqla_demo/models.py deleted file mode 100644 index 3dfb40e58..000000000 --- a/docs/quick_tour/sqla_demo/sqla_demo/models.py +++ /dev/null @@ -1,29 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - -# Start Sphinx Include -class MyModel(Base): - __tablename__ = 'models' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - value = Column(Integer) - - def __init__(self, name, value): - self.name = name - self.value = value - # End Sphinx Include diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py new file mode 100644 index 000000000..6ffc10a78 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import MyModel # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py @@ -0,0 +1,46 @@ +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 + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py b/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py new file mode 100644 index 000000000..eb645bfe6 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py @@ -0,0 +1,19 @@ +from .meta import Base +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + + +# Start Sphinx Include +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + # End Sphinx Include + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py b/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py index 66feb3008..f0d09729e 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py @@ -2,36 +2,44 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, ) -from ..models import ( - DBSession, - MyModel, +from pyramid.scripts.common import parse_vars + +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import MyModel def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s \n' + print('usage: %s [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = MyModel(name='one', value=1) - DBSession.add(model) + dbsession.add(model) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/favicon.ico b/docs/quick_tour/sqla_demo/sqla_demo/static/favicon.ico deleted file mode 100644 index 71f837c9e..000000000 Binary files a/docs/quick_tour/sqla_demo/sqla_demo/static/favicon.ico and /dev/null differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/footerbg.png b/docs/quick_tour/sqla_demo/sqla_demo/static/footerbg.png deleted file mode 100644 index 1fbc873da..000000000 Binary files a/docs/quick_tour/sqla_demo/sqla_demo/static/footerbg.png and /dev/null differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/headerbg.png b/docs/quick_tour/sqla_demo/sqla_demo/static/headerbg.png deleted file mode 100644 index 0596f2020..000000000 Binary files a/docs/quick_tour/sqla_demo/sqla_demo/static/headerbg.png and /dev/null differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css b/docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/quick_tour/sqla_demo/sqla_demo/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/middlebg.png b/docs/quick_tour/sqla_demo/sqla_demo/static/middlebg.png deleted file mode 100644 index 2369cfb7d..000000000 Binary files a/docs/quick_tour/sqla_demo/sqla_demo/static/middlebg.png and /dev/null differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pylons.css b/docs/quick_tour/sqla_demo/sqla_demo/static/pylons.css deleted file mode 100644 index 4b1c017cd..000000000 --- a/docs/quick_tour/sqla_demo/sqla_demo/static/pylons.css +++ /dev/null @@ -1,372 +0,0 @@ -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td -{ - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; /* 16px */ - vertical-align: baseline; - background: transparent; -} - -body -{ - line-height: 1; -} - -ol, ul -{ - list-style: none; -} - -blockquote, q -{ - quotes: none; -} - -blockquote:before, blockquote:after, q:before, q:after -{ - content: ''; - content: none; -} - -:focus -{ - outline: 0; -} - -ins -{ - text-decoration: none; -} - -del -{ - text-decoration: line-through; -} - -table -{ - border-collapse: collapse; - border-spacing: 0; -} - -sub -{ - vertical-align: sub; - font-size: smaller; - line-height: normal; -} - -sup -{ - vertical-align: super; - font-size: smaller; - line-height: normal; -} - -ul, menu, dir -{ - display: block; - list-style-type: disc; - margin: 1em 0; - padding-left: 40px; -} - -ol -{ - display: block; - list-style-type: decimal-leading-zero; - margin: 1em 0; - padding-left: 40px; -} - -li -{ - display: list-item; -} - -ul ul, ul ol, ul dir, ul menu, ul dl, ol ul, ol ol, ol dir, ol menu, ol dl, dir ul, dir ol, dir dir, dir menu, dir dl, menu ul, menu ol, menu dir, menu menu, menu dl, dl ul, dl ol, dl dir, dl menu, dl dl -{ - margin-top: 0; - margin-bottom: 0; -} - -ol ul, ul ul, menu ul, dir ul, ol menu, ul menu, menu menu, dir menu, ol dir, ul dir, menu dir, dir dir -{ - list-style-type: circle; -} - -ol ol ul, ol ul ul, ol menu ul, ol dir ul, ol ol menu, ol ul menu, ol menu menu, ol dir menu, ol ol dir, ol ul dir, ol menu dir, ol dir dir, ul ol ul, ul ul ul, ul menu ul, ul dir ul, ul ol menu, ul ul menu, ul menu menu, ul dir menu, ul ol dir, ul ul dir, ul menu dir, ul dir dir, menu ol ul, menu ul ul, menu menu ul, menu dir ul, menu ol menu, menu ul menu, menu menu menu, menu dir menu, menu ol dir, menu ul dir, menu menu dir, menu dir dir, dir ol ul, dir ul ul, dir menu ul, dir dir ul, dir ol menu, dir ul menu, dir menu menu, dir dir menu, dir ol dir, dir ul dir, dir menu dir, dir dir dir -{ - list-style-type: square; -} - -.hidden -{ - display: none; -} - -p -{ - line-height: 1.5em; -} - -h1 -{ - font-size: 1.75em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h2 -{ - font-size: 1.5em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h3 -{ - font-size: 1.25em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h4 -{ - font-size: 1em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -html, body -{ - width: 100%; - height: 100%; -} - -body -{ - margin: 0; - padding: 0; - background-color: #fff; - position: relative; - font: 16px/24px NobileRegular, "Lucida Grande", Lucida, Verdana, sans-serif; -} - -a -{ - color: #1b61d6; - text-decoration: none; -} - -a:hover -{ - color: #e88f00; - text-decoration: underline; -} - -body h1, body h2, body h3, body h4, body h5, body h6 -{ - font-family: NeutonRegular, "Lucida Grande", Lucida, Verdana, sans-serif; - font-weight: 400; - color: #373839; - font-style: normal; -} - -#wrap -{ - min-height: 100%; -} - -#header, #footer -{ - width: 100%; - color: #fff; - height: 40px; - position: absolute; - text-align: center; - line-height: 40px; - overflow: hidden; - font-size: 12px; - vertical-align: middle; -} - -#header -{ - background: #000; - top: 0; - font-size: 14px; -} - -#footer -{ - bottom: 0; - background: #000 url(footerbg.png) repeat-x 0 top; - position: relative; - margin-top: -40px; - clear: both; -} - -.header, .footer -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.wrapper -{ - width: 100%; -} - -#top, #top-small, #bottom -{ - width: 100%; -} - -#top -{ - color: #000; - height: 230px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#top-small -{ - color: #000; - height: 60px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#bottom -{ - color: #222; - background-color: #fff; -} - -.top, .top-small, .middle, .bottom -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.top -{ - padding-top: 40px; -} - -.top-small -{ - padding-top: 10px; -} - -#middle -{ - width: 100%; - height: 100px; - background: url(middlebg.png) repeat-x; - border-top: 2px solid #fff; - border-bottom: 2px solid #b2b2b2; -} - -.app-welcome -{ - margin-top: 25px; -} - -.app-name -{ - color: #000; - font-weight: 700; -} - -.bottom -{ - padding-top: 50px; -} - -#left -{ - width: 350px; - float: left; - padding-right: 25px; -} - -#right -{ - width: 350px; - float: right; - padding-left: 25px; -} - -.align-left -{ - text-align: left; -} - -.align-right -{ - text-align: right; -} - -.align-center -{ - text-align: center; -} - -ul.links -{ - margin: 0; - padding: 0; -} - -ul.links li -{ - list-style-type: none; - font-size: 14px; -} - -form -{ - border-style: none; -} - -fieldset -{ - border-style: none; -} - -input -{ - color: #222; - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 12px; - line-height: 16px; -} - -input[type=text], input[type=password] -{ - width: 205px; -} - -input[type=submit] -{ - background-color: #ddd; - font-weight: 700; -} - -/*Opera Fix*/ -body:before -{ - content: ""; - height: 100%; - float: left; - width: 0; - margin-top: -32767px; -} diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-small.png b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-small.png deleted file mode 100644 index a5bc0ade7..000000000 Binary files a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-small.png and /dev/null differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png index 347e05549..4ab837be9 100644 Binary files a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png and b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/theme.css b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/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/quick_tour/sqla_demo/sqla_demo/static/theme.min.css b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css new file mode 100644 index 000000000..0d25de5b6 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/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/quick_tour/sqla_demo/sqla_demo/static/transparent.gif b/docs/quick_tour/sqla_demo/sqla_demo/static/transparent.gif deleted file mode 100644 index 0341802e5..000000000 Binary files a/docs/quick_tour/sqla_demo/sqla_demo/static/transparent.gif and /dev/null differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2 b/docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2 new file mode 100644 index 000000000..76a098122 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2 b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

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

+
+{% endblock content %} diff --git a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt deleted file mode 100644 index 321c0f5fb..000000000 --- a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt +++ /dev/null @@ -1,76 +0,0 @@ - - - - The Pyramid Web Framework - - - - - - - - - - -
-
-
-
pyramid
-
-
-
-
-

- Welcome to ${project}, an application generated by
- the Pyramid web framework. -

-
-
-
-
-
-

Search documentation

-
- - -
-
- -
-
-
- - - diff --git a/docs/quick_tour/sqla_demo/sqla_demo/tests.py b/docs/quick_tour/sqla_demo/sqla_demo/tests.py index 6fef6d695..b6b6fdf4d 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/tests.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/tests.py @@ -3,31 +3,63 @@ import transaction from pyramid import testing -from .models import DBSession +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) -class TestMyView(unittest.TestCase): + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - from .models import ( - Base, - MyModel, + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() + + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, ) - DBSession.configure(bind=engine) - Base.metadata.create_all(engine) - with transaction.manager: - model = MyModel(name='one', value=55) - DBSession.add(model) + + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) + + self.session = get_session(transaction.manager, dbmaker) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - DBSession.remove() + from .models.meta import Base + testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def test_it(self): - from .views import my_view - request = testing.DummyRequest() - info = my_view(request) + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models.mymodel 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'], 'sqla_demo') + + +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/quick_tour/sqla_demo/sqla_demo/views.py b/docs/quick_tour/sqla_demo/sqla_demo/views.py deleted file mode 100644 index 768a7e42e..000000000 --- a/docs/quick_tour/sqla_demo/sqla_demo/views.py +++ /dev/null @@ -1,37 +0,0 @@ -from pyramid.response import Response -from pyramid.view import view_config - -from sqlalchemy.exc import DBAPIError - -from .models import ( - DBSession, - MyModel, - ) - - -@view_config(route_name='home', renderer='templates/mytemplate.pt') -def my_view(request): - try: - # Start Sphinx Include - one = DBSession.query(MyModel).filter(MyModel.name == 'one').first() - # End Sphinx Include - except DBAPIError: - return Response(conn_err_msg, content_type='text/plain', status_int=500) - return {'one': one, 'project': 'sqla_demo'} - -conn_err_msg = """\ -Pyramid is having a problem using your SQL database. The problem -might be caused by one of the following things: - -1. You may need to run the "initialize_sqla_demo_db" script - to initialize your database tables. Check your virtual - environment's "bin" directory for this script and try to run it. - -2. Your database server may not be running. Check that the - database server referred to by the "sqlalchemy.url" setting in - your "development.ini" file is running. - -After you fix the problem, please restart the Pyramid application to -try it again. -""" - diff --git a/docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/quick_tour/sqla_demo/sqla_demo/views/default.py b/docs/quick_tour/sqla_demo/sqla_demo/views/default.py new file mode 100644 index 000000000..e5e70cf9d --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/views/default.py @@ -0,0 +1,35 @@ +from pyramid.response import Response +from pyramid.view import view_config + +from sqlalchemy.exc import DBAPIError + +from ..models.mymodel import MyModel + + +@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') +def my_view(request): + try: + query = request.dbsession.query(MyModel) + # Start Sphinx Include + one = query.filter(MyModel.name == 'one').first() + # End Sphinx Include + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status_int=500) + return {'one': one, 'project': 'sqla_demo'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_sqla_demo_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 649c11e85..abe9e4202 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -12,12 +12,12 @@ Application configuration with ``__init__.py`` A directory on disk can be turned into a Python :term:`package` by containing an ``__init__.py`` file. Even if empty, this marks a directory as a Python -package. We use ``__init__.py`` both as a marker, indicating the directory -in which it's contained is a package, and to contain application configuration +package. We use ``__init__.py`` both as a marker, indicating the directory in +which it's contained is a package, and to contain application configuration code. -Open ``tutorial/tutorial/__init__.py``. It should already contain -the following: +Open ``tutorial/tutorial/__init__.py``. It should already contain the +following: .. literalinclude:: src/basiclayout/tutorial/__init__.py :linenos: @@ -43,58 +43,37 @@ When you invoke the ``pserve development.ini`` command, the ``main`` function above is executed. It accepts some settings and returns a :term:`WSGI` application. (See :ref:`startup_chapter` for more about ``pserve``.) -The main function first creates a :term:`SQLAlchemy` database engine using -:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.`` prefixed -settings in the ``development.ini`` file's ``[app:main]`` section. -This will be a URI (something like ``sqlite://``): - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 - :language: py - -``main`` then initializes our SQLAlchemy session object, passing it the -engine: - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 14 - :language: py - -``main`` subsequently initializes our SQLAlchemy declarative ``Base`` object, -assigning the engine we created to the ``bind`` attribute of it's -``metadata`` object. This allows table definitions done imperatively -(instead of declaratively, via a class statement) to work. We won't use any -such tables in our application, but if you add one later, long after you've -forgotten about this tutorial, you won't be left scratching your head when it -doesn't work. - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 15 - :language: py - The next step of ``main`` is to construct a :term:`Configurator` object: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 16 + :lines: 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 dictionary of settings parsed from the ``.ini`` file, which contains deployment-related values such as ``pyramid.reload_templates``, -``db_string``, etc. +``sqlalchemy.url``, and so on. -Next, include :term:`Chameleon` templating bindings so that we can use -renderers with the ``.pt`` extension within our project. +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: 17 + :lines: 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 :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: 18 + :lines: 10 :language: py This registers a static resource view which will match any URL that starts @@ -112,11 +91,11 @@ via the :meth:`pyramid.config.Configurator.add_route` method that will be used when the URL is ``/``: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 19 + :lines: 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/``. +Since this route has a ``pattern`` equaling ``/``, it is the route that will +be matched when the URL ``/`` is visited, e.g., ``http://localhost:6543/``. ``main`` next calls the ``scan`` method of the configurator (:meth:`pyramid.config.Configurator.scan`), which will recursively scan our @@ -126,10 +105,10 @@ view configuration will be registered, which will allow one of our application URLs to be mapped to some code. .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 20 + :lines: 12 :language: py -Finally, ``main`` is finished configuring things, so it uses the +Finally ``main`` is finished configuring things, so it uses the :meth:`pyramid.config.Configurator.make_wsgi_app` method to return a :term:`WSGI` application: @@ -187,6 +166,39 @@ to inform the user about possible actions to take to solve the problem. Content models with the ``models`` package ------------------------------------------ +.. START moved from Application configuration with ``__init__.py``. This + section is a WIP, and needs to be updated using the new models package. + +The main function first creates a :term:`SQLAlchemy` database engine using +:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.`` prefixed +settings in the ``development.ini`` file's ``[app:main]`` section. +This will be a URI (something like ``sqlite://``): + + .. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 13 + :language: py + +``main`` then initializes our SQLAlchemy session object, passing it the +engine: + + .. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 14 + :language: py + +``main`` subsequently initializes our SQLAlchemy declarative ``Base`` object, +assigning the engine we created to the ``bind`` attribute of it's +``metadata`` object. This allows table definitions done imperatively +(instead of declaratively, via a class statement) to work. We won't use any +such tables in our application, but if you add one later, long after you've +forgotten about this tutorial, you won't be left scratching your head when it +doesn't work. + + .. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 15 + :language: py + +.. END moved from Application configuration with ``__init__.py`` + 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. diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini index a9d53b296..99c4ff0fe 100644 --- a/docs/tutorials/wiki2/src/basiclayout/development.ini +++ b/docs/tutorials/wiki2/src/basiclayout/development.ini @@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index fa94c1b3e..97acfbd7d 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -59,4 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py index 867049e4f..7994bbfa8 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py @@ -1,20 +1,12 @@ from pyramid.config import Configurator -from sqlalchemy import engine_from_config - -from .models import ( - DBSession, - Base, - ) def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine config = Configurator(settings=settings) - config.include('pyramid_chameleon') + config.include('pyramid_jinja2') + config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/pyramid/scaffolds/alchemy/+package+/__init__.py b/pyramid/scaffolds/alchemy/+package+/__init__.py index 867049e4f..7994bbfa8 100644 --- a/pyramid/scaffolds/alchemy/+package+/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/__init__.py @@ -1,20 +1,12 @@ from pyramid.config import Configurator -from sqlalchemy import engine_from_config - -from .models import ( - DBSession, - Base, - ) def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine config = Configurator(settings=settings) - config.include('pyramid_chameleon') + config.include('pyramid_jinja2') + config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/pyramid/scaffolds/alchemy/+package+/models.py b/pyramid/scaffolds/alchemy/+package+/models.py deleted file mode 100644 index a0d3e7b71..000000000 --- a/pyramid/scaffolds/alchemy/+package+/models.py +++ /dev/null @@ -1,27 +0,0 @@ -from sqlalchemy import ( - Column, - Index, - Integer, - Text, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class MyModel(Base): - __tablename__ = 'models' - id = Column(Integer, primary_key=True) - name = Column(Text) - value = Column(Integer) - -Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py b/pyramid/scaffolds/alchemy/+package+/models/__init__.py new file mode 100644 index 000000000..6ffc10a78 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import MyModel # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/pyramid/scaffolds/alchemy/+package+/models/meta.py b/pyramid/scaffolds/alchemy/+package+/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/models/meta.py @@ -0,0 +1,46 @@ +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 + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/pyramid/scaffolds/alchemy/+package+/models/mymodel.py b/pyramid/scaffolds/alchemy/+package+/models/mymodel.py new file mode 100644 index 000000000..5a2b5890c --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/models/mymodel.py @@ -0,0 +1,17 @@ +from .meta import Base +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index 7dfdece15..f0d09729e 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -2,8 +2,6 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, @@ -11,11 +9,13 @@ from pyramid.paster import ( from pyramid.scripts.common import parse_vars -from ..models import ( - DBSession, - MyModel, +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import MyModel def usage(argv): @@ -32,9 +32,14 @@ def main(argv=sys.argv): options = parse_vars(argv[2:]) setup_logging(config_uri) settings = get_appsettings(config_uri, options=options) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = MyModel(name='one', value=1) - DBSession.add(model) + dbsession.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl new file mode 100644 index 000000000..51e382654 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl new file mode 100644 index 000000000..01fe5b8e3 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

Welcome to \{\{project\}\}, an application generated by
the Pyramid Web Framework {{pyramid_version}}.

+
+{% endblock content %} diff --git a/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl deleted file mode 100644 index 3f1d23d47..000000000 --- a/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

Welcome to ${project}, an application generated by
the Pyramid Web Framework {{pyramid_version}}.

-
-
-
-
- -
-
- -
-
-
- - - - - - - - diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index e6425eb91..074c7a773 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -3,53 +3,63 @@ import transaction from pyramid import testing -from .models import DBSession +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) -class TestMyViewSuccessCondition(unittest.TestCase): + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - from .models import ( - Base, - MyModel, + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() + + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, ) - DBSession.configure(bind=engine) - Base.metadata.create_all(engine) - with transaction.manager: - model = MyModel(name='one', value=55) - DBSession.add(model) + + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) + + self.session = get_session(transaction.manager, dbmaker) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - DBSession.remove() + from .models.meta import Base + testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models.mymodel import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) def test_passing_view(self): - from .views import my_view - request = testing.DummyRequest() - info = my_view(request) + from .views.default import my_view + info = my_view(dummy_request(self.session)) self.assertEqual(info['one'].name, 'one') self.assertEqual(info['project'], '{{project}}') -class TestMyViewFailureCondition(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - from .models import ( - Base, - MyModel, - ) - DBSession.configure(bind=engine) - - def tearDown(self): - DBSession.remove() - testing.tearDown() +class TestMyViewFailureCondition(BaseTest): def test_failing_view(self): - from .views import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info.status_int, 500) \ No newline at end of file + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/pyramid/scaffolds/alchemy/+package+/views.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views.py_tmpl deleted file mode 100644 index 292bce579..000000000 --- a/pyramid/scaffolds/alchemy/+package+/views.py_tmpl +++ /dev/null @@ -1,36 +0,0 @@ -from pyramid.response import Response -from pyramid.view import view_config - -from sqlalchemy.exc import DBAPIError - -from .models import ( - DBSession, - MyModel, - ) - - -@view_config(route_name='home', renderer='templates/mytemplate.pt') -def my_view(request): - try: - one = DBSession.query(MyModel).filter(MyModel.name == 'one').first() - except DBAPIError: - return Response(conn_err_msg, content_type='text/plain', status_int=500) - return {'one': one, 'project': '{{project}}'} - - -conn_err_msg = """\ -Pyramid is having a problem using your SQL database. The problem -might be caused by one of the following things: - -1. You may need to run the "initialize_{{project}}_db" script - to initialize your database tables. Check your virtual - environment's "bin" directory for this script and try to run it. - -2. Your database server may not be running. Check that the - database server referred to by the "sqlalchemy.url" setting in - your "development.ini" file is running. - -After you fix the problem, please restart the Pyramid application to -try it again. -""" - diff --git a/pyramid/scaffolds/alchemy/+package+/views/__init__.py b/pyramid/scaffolds/alchemy/+package+/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl new file mode 100644 index 000000000..43fb33e05 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl @@ -0,0 +1,33 @@ +from pyramid.response import Response +from pyramid.view import view_config + +from sqlalchemy.exc import DBAPIError + +from ..models.mymodel import MyModel + + +@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') +def my_view(request): + try: + query = request.dbsession.query(MyModel) + one = query.filter(MyModel.name == 'one').first() + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status_int=500) + return {'one': one, 'project': '{{project}}'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_{{project}}_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index 9496b9948..af193a73d 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'pyramid_tm', 'SQLAlchemy', -- cgit v1.2.3 From 3214690b08e3c495ab64170e1430ece240853110 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 13 Nov 2015 02:30:32 -0800 Subject: update wiki2/src/views and wiki2/definingviews.rst --- docs/tutorials/wiki2/definingviews.rst | 12 +- docs/tutorials/wiki2/src/views/development.ini | 4 +- docs/tutorials/wiki2/src/views/production.ini | 14 +- docs/tutorials/wiki2/src/views/setup.py | 2 +- .../tutorials/wiki2/src/views/tutorial/__init__.py | 12 +- docs/tutorials/wiki2/src/views/tutorial/models.py | 25 --- .../src/views/tutorial/scripts/initializedb.py | 25 +-- .../wiki2/src/views/tutorial/templates/edit.pt | 69 -------- .../src/views/tutorial/templates/mytemplate.pt | 66 -------- .../wiki2/src/views/tutorial/templates/view.pt | 69 -------- docs/tutorials/wiki2/src/views/tutorial/tests.py | 175 ++++++--------------- docs/tutorials/wiki2/src/views/tutorial/views.py | 72 --------- 12 files changed, 83 insertions(+), 462 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/models.py delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/view.pt delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/views.py diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 0b495445a..8a96e433c 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -14,7 +14,7 @@ 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 +Declaring dependencies in our ``setup.py`` file =============================================== The view code in our application will depend on a package which is not a @@ -47,7 +47,7 @@ directory in which ``setup.py`` lives) and execute the following command. On UNIX: -.. code-block:: text +.. code-block:: bash $ cd tutorial $ $VENV/bin/python setup.py develop @@ -60,17 +60,17 @@ On Windows: c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop Success executing this command will end with a line to the console something -like:: +like this:: Finished processing dependencies for tutorial==0.0 Adding view functions in ``views.py`` ===================================== -It's time for a major change. Open ``tutorial/tutorial/views.py`` and edit it -to look like the following: +It's time for a major change. Open ``tutorial/tutorial/views/default.py`` and +edit it to look like the following: -.. literalinclude:: src/views/tutorial/views.py +.. literalinclude:: src/views/tutorial/views/default.py :linenos: :language: python :emphasize-lines: 1-7,14,16-72 diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini index a9d53b296..99c4ff0fe 100644 --- a/docs/tutorials/wiki2/src/views/development.ini +++ b/docs/tutorials/wiki2/src/views/development.ini @@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini index 4684d2f7a..97acfbd7d 100644 --- a/docs/tutorials/wiki2/src/views/production.ini +++ b/docs/tutorials/wiki2/src/views/production.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -16,7 +21,10 @@ use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -51,6 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index 09bd63d33..d4e5a4072 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'pyramid_tm', 'SQLAlchemy', diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index 37cae1997..d28f09ca4 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -1,20 +1,12 @@ from pyramid.config import Configurator -from sqlalchemy import engine_from_config - -from .models import ( - DBSession, - Base, - ) def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine config = Configurator(settings=settings) - config.include('pyramid_chameleon') + config.include('pyramid_jinja2') + config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('view_page', '/{pagename}') diff --git a/docs/tutorials/wiki2/src/views/tutorial/models.py b/docs/tutorials/wiki2/src/views/tutorial/models.py deleted file mode 100644 index f028c917a..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/models.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py index 23a5f13f4..4aac4a848 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py @@ -2,36 +2,41 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, ) -from ..models import ( - DBSession, - Page, +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import Page def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s \n' + print('usage: %s [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] setup_logging(config_uri) settings = get_appsettings(config_uri) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt deleted file mode 100644 index c0c1b6c20..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Editing Page Name Goes - Here -

-

You can return to the - FrontPage. -

-
-
- -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt deleted file mode 100644 index c9b0cec21..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

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

-
-
-
- -
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt deleted file mode 100644 index 0f564b16c..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-
- Page text goes here. -
-

- - Edit this page - -

-

- Viewing - Page Name Goes Here -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py index 9f01d2da5..b947e3bb1 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py @@ -3,144 +3,63 @@ import transaction from pyramid import testing -def _initTestingDB(): - from sqlalchemy import create_engine - from tutorial.models import ( - DBSession, - Page, - Base - ) - engine = create_engine('sqlite://') - Base.metadata.create_all(engine) - DBSession.configure(bind=engine) - with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) - return DBSession - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - -class ViewWikiTests(unittest.TestCase): + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - self.session = _initTestingDB() + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, + ) - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') + self.session = get_session(transaction.manager, dbmaker) -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '
\n' - '

Hello ' - 'CruelWorld ' - '' - 'IDoExist' - '

\n
\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - - -class AddPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + from .models.meta import Base - def tearDown(self): - self.session.remove() testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - -class EditPageTests(unittest.TestCase): def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models.mymodel import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], - 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py deleted file mode 100644 index a3707dab5..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ /dev/null @@ -1,72 +0,0 @@ -import cgi -import re -from docutils.core import publish_parts - -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) - -from pyramid.view import view_config - -from .models import ( - DBSession, - Page, - ) - -# regular expression used to find WikiWords -wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - -@view_config(route_name='view_wiki') -def view_wiki(request): - return HTTPFound(location = request.route_url('view_page', - pagename='FrontPage')) - -@view_config(route_name='view_page', renderer='templates/view.pt') -def view_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).first() - if page is None: - return HTTPNotFound('No such page') - - def check(match): - word = match.group(1) - exists = DBSession.query(Page).filter_by(name=word).all() - if exists: - view_url = request.route_url('view_page', pagename=word) - return '%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(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url) - -@view_config(route_name='add_page', renderer='templates/edit.pt') -def add_page(request): - pagename = request.matchdict['pagename'] - if 'form.submitted' in request.params: - body = request.params['body'] - page = Page(name=pagename, data=body) - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url) - -@view_config(route_name='edit_page', renderer='templates/edit.pt') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - return dict( - page=page, - save_url = request.route_url('edit_page', pagename=pagename), - ) -- cgit v1.2.3 From 429529a09992554229ef7274bfc4f43394256105 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 13 Nov 2015 03:46:13 -0800 Subject: update wiki2/src/views and wiki2/definingviews.rst --- docs/tutorials/wiki2/definingviews.rst | 113 +++++++++++++++++---------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 8a96e433c..08fa8f16b 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -64,8 +64,8 @@ like this:: Finished processing dependencies for tutorial==0.0 -Adding view functions in ``views.py`` -===================================== +Adding view functions in ``views/default.py`` +============================================= It's time for a major change. Open ``tutorial/tutorial/views/default.py`` and edit it to look like the following: @@ -73,18 +73,19 @@ edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py :linenos: :language: python - :emphasize-lines: 1-7,14,16-72 + :emphasize-lines: 1-9,12-70 The highlighted lines need to be added or edited. -We added some imports and created a regular expression to find "WikiWords". +We added some imports, and created a regular expression to find "WikiWords". We got rid of the ``my_view`` view function and its decorator that was added when we originally rendered the ``alchemy`` scaffold. It was only an example -and isn't relevant to our application. +and isn't relevant to our application. We also delated the ``db_err_msg`` +string. -Then we added four :term:`view callable` functions to our ``views.py`` -module: +Then we added four :term:`view callable` functions to our ``views/default.py`` +module: * ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. * ``view_page()`` - Displays an individual page. @@ -95,20 +96,20 @@ We'll describe each one briefly in the following sections. .. note:: - There is nothing special about the filename ``views.py``. A project may - have many view callables throughout its codebase in arbitrarily named - files. Files implementing view callables often have ``view`` in their - filenames (or may live in a Python subpackage of your application package - named ``views``), but this is only by convention. + There is nothing special about the filename ``default.py``. A project may + have many view callables throughout its codebase in arbitrarily named files. + Files implementing view callables often have ``view`` in their filenames (or + may live in a Python subpackage of your application package named ``views``, + as in our case), but this is only by convention. The ``view_wiki`` view function ------------------------------- Following is the code for the ``view_wiki`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :lines: 20-24 - :lineno-start: 20 +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 17-20 + :lineno-start: 17 :linenos: :language: python @@ -130,9 +131,9 @@ The ``view_page`` view function Here is the code for the ``view_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :lines: 25-45 - :lineno-start: 25 +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 22-42 + :lineno-start: 22 :linenos: :language: python @@ -158,17 +159,17 @@ template, and we return a dictionary with a number of arguments. The fact that ``view_page()`` returns a dictionary (as opposed to a :term:`response` object) is a cue to :app:`Pyramid` that it should try to use a :term:`renderer` associated with the view configuration to render a response. In our case, the -renderer used will be the ``templates/view.pt`` template, as indicated in the -``@view_config`` decorator that is applied to ``view_page()``. +renderer used will be the ``templates/view.jinja2`` template, as indicated in +the ``@view_config`` decorator that is applied to ``view_page()``. The ``add_page`` view function ------------------------------ Here is the code for the ``add_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :lines: 47-58 - :lineno-start: 47 +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 44-55 + :lineno-start: 44 :linenos: :language: python @@ -189,15 +190,15 @@ If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), we grab the page body from the form data, create a Page object with this page body and the name taken from ``matchdict['pagename']``, and save it into the database using -``DBSession.add``. We then redirect back to the ``view_page`` view for the -newly created page. +``request.dbession.add``. We then redirect back to the ``view_page`` view for +the newly created page. If the view execution is *not* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``False``), the view callable renders a template. To do so, it generates a ``save_url`` which the template uses as the form post URL during rendering. We're lazy here, so -we're going to use the same template (``templates/edit.pt``) for the add -view as well as the page edit view. To do so we create a dummy Page object +we're going to use the same template (``templates/edit.jinja2``) for the add +view as well as the page edit view. To do so we create a dummy ``Page`` object in order to satisfy the edit form's desire to have *some* page object exposed as ``page``. :app:`Pyramid` will render the template associated with this view to a response. @@ -207,14 +208,14 @@ The ``edit_page`` view function Here is the code for the ``edit_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :lines: 60-72 - :lineno-start: 60 +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 57-69 + :lineno-start: 57 :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 +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. @@ -234,49 +235,51 @@ Adding templates ================ The ``view_page``, ``add_page`` and ``edit_page`` views that we've added -reference a :term:`template`. Each template is a :term:`Chameleon` -:term:`ZPT` template. These templates will live in the ``templates`` -directory of our tutorial package. Chameleon templates must have a ``.pt`` -extension to be recognized as such. +reference a :term:`template`. Each template is a :term:`Jinja2` template. +These templates will live in the ``templates`` directory of our tutorial +package. Jinja2 templates must have a ``.jinja2`` extension to be recognized +as such. -The ``view.pt`` template ------------------------- +The ``view.jinja2`` template +---------------------------- -Create ``tutorial/tutorial/templates/view.pt`` and add the following +Create ``tutorial/tutorial/templates/view.jinja2`` and add the following content: -.. literalinclude:: src/views/tutorial/templates/view.pt +.. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: + :emphasize-lines: 36,38-40 :language: html This template is used by ``view_page()`` for displaying a single wiki page. It includes: -- A ``div`` element that is replaced with the ``content`` value provided by - the view (lines 36-38). ``content`` contains HTML, so the ``structure`` - keyword is used to prevent escaping it (i.e., changing ">" to ">", etc.) -- A link that points at the "edit" URL which invokes the ``edit_page`` view - for the page being viewed (lines 40-42). +- 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 + 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 ``edit.pt`` template ------------------------- +The ``edit.jinja2`` template +---------------------------- -Create ``tutorial/tutorial/templates/edit.pt`` and add the following +Create ``tutorial/tutorial/templates/edit.jinja2`` and add the following content: -.. literalinclude:: src/views/tutorial/templates/edit.pt +.. literalinclude:: src/views/tutorial/templates/edit.jinja2 :linenos: + :emphasize-lines: 42,44,47 :language: html This template is used by ``add_page()`` and ``edit_page()`` for adding and editing a wiki page. It displays a page containing a form that includes: -- A 10 row by 60 column ``textarea`` field named ``body`` that is filled - with any existing page data when it is rendered (line 45). -- A submit button that has the name ``form.submitted`` (line 48). +- 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). The form POSTs back to the ``save_url`` argument supplied by the view (line -43). The view will use the ``body`` and ``form.submitted`` values. +42). The view will use the ``body`` and ``form.submitted`` values. .. note:: Our templates use a ``request`` object that none of our tutorial views return in their dictionary. ``request`` is one of several names that @@ -284,7 +287,7 @@ The form POSTs back to the ``save_url`` argument supplied by the view (line See :ref:`renderer_system_values` for information about other names that are available by default when a template is used as a renderer. -Static Assets +Static assets ------------- Our templates name static assets, including CSS and images. We don't need @@ -299,7 +302,7 @@ 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 ``__init__.py`` ================================ The ``__init__.py`` file contains @@ -340,7 +343,7 @@ something like: .. literalinclude:: src/views/tutorial/__init__.py :linenos: - :emphasize-lines: 19-22 + :emphasize-lines: 11-14 :language: python The highlighted lines are the ones that need to be added or edited. -- cgit v1.2.3 From 3525fa106fe2ca4b4bed93e28f72b13e3fa943b3 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 13 Nov 2015 03:54:14 -0800 Subject: correct line numbers in and remove obsolete WIP block wiki2/basiclayout.rst --- docs/tutorials/wiki2/basiclayout.rst | 45 ++++++++++-------------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index abe9e4202..1ba7d3958 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -36,6 +36,7 @@ the ``main`` function we've defined in our ``__init__.py``: .. literalinclude:: src/basiclayout/tutorial/__init__.py :pyobject: main + :lineno-start: 4 :linenos: :language: py @@ -47,6 +48,7 @@ The next step of ``main`` is to construct a :term:`Configurator` object: .. 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 @@ -60,6 +62,7 @@ with the ``.jinja2`` extension within our project. .. 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 @@ -67,6 +70,7 @@ Python path. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 9 + :lineno-start: 9 :language: py ``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with @@ -74,6 +78,7 @@ two arguments: ``static`` (the name), and ``static`` (the path): .. 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 @@ -92,6 +97,7 @@ used when the URL is ``/``: .. 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 @@ -106,6 +112,7 @@ application URLs to be mapped to some code. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 12 + :lineno-start: 12 :language: py Finally ``main`` is finished configuring things, so it uses the @@ -114,6 +121,7 @@ Finally ``main`` is finished configuring things, so it uses the .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 13 + :lineno-start: 13 :language: py @@ -166,39 +174,6 @@ to inform the user about possible actions to take to solve the problem. Content models with the ``models`` package ------------------------------------------ -.. START moved from Application configuration with ``__init__.py``. This - section is a WIP, and needs to be updated using the new models package. - -The main function first creates a :term:`SQLAlchemy` database engine using -:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.`` prefixed -settings in the ``development.ini`` file's ``[app:main]`` section. -This will be a URI (something like ``sqlite://``): - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 - :language: py - -``main`` then initializes our SQLAlchemy session object, passing it the -engine: - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 14 - :language: py - -``main`` subsequently initializes our SQLAlchemy declarative ``Base`` object, -assigning the engine we created to the ``bind`` attribute of it's -``metadata`` object. This allows table definitions done imperatively -(instead of declaratively, via a class statement) to work. We won't use any -such tables in our application, but if you add one later, long after you've -forgotten about this tutorial, you won't be left scratching your head when it -doesn't work. - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 15 - :language: py - -.. END moved from Application configuration with ``__init__.py`` - 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. @@ -246,6 +221,7 @@ configures various database settings by calling subsequently defined functions. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: includeme + :lineno-start: 19 :linenos: :language: py @@ -256,6 +232,7 @@ 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: 32 :linenos: :language: py @@ -266,6 +243,7 @@ URI, something like ``sqlite://``. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_engine + :lineno-start: 39 :linenos: :language: py @@ -276,6 +254,7 @@ creating a session with the database engine. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_dbmaker + :lineno-start: 43 :linenos: :language: py -- cgit v1.2.3 From 2f6864759d66b8a9cea845d8e6b628b5bede5336 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 14 Nov 2015 03:15:59 -0800 Subject: minor changes to narrative flow --- docs/tutorials/wiki2/basiclayout.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 1ba7d3958..1426e55a5 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -44,7 +44,7 @@ When you invoke the ``pserve development.ini`` command, the ``main`` function above is executed. It accepts some settings and returns a :term:`WSGI` application. (See :ref:`startup_chapter` for more about ``pserve``.) -The next step of ``main`` is to construct a :term:`Configurator` object: +Next in ``main``, construct a :term:`Configurator` object: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 7 @@ -279,8 +279,8 @@ The ``MyModel`` class has a ``__tablename__`` attribute. This informs SQLAlchemy which table to use to store the data representing instances of this class. -The Index import and the Index object creation is not required for this -tutorial, and will be removed in the next step. - 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. -- cgit v1.2.3 From 73c256efdbe0b5663c726623b95c05e800939fbc Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 14 Nov 2015 03:51:45 -0800 Subject: commit new wiki2/src/views/tutorial files --- .../wiki2/src/views/tutorial/models/__init__.py | 7 +++ .../wiki2/src/views/tutorial/models/meta.py | 46 +++++++++++++++ .../wiki2/src/views/tutorial/models/mymodel.py | 14 +++++ .../wiki2/src/views/tutorial/templates/edit.jinja2 | 68 +++++++++++++++++++++ .../src/views/tutorial/templates/layout.jinja2 | 66 +++++++++++++++++++++ .../src/views/tutorial/templates/mytemplate.jinja2 | 8 +++ .../wiki2/src/views/tutorial/templates/view.jinja2 | 66 +++++++++++++++++++++ .../wiki2/src/views/tutorial/views/__init__.py | 0 .../wiki2/src/views/tutorial/views/default.py | 69 ++++++++++++++++++++++ 9 files changed, 344 insertions(+) create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/default.py diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py new file mode 100644 index 000000000..7b1c62867 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import Page # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py @@ -0,0 +1,46 @@ +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 + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py new file mode 100644 index 000000000..45571d78e --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py @@ -0,0 +1,14 @@ +from .meta import Base +from sqlalchemy import ( + Column, + Integer, + Text, +) + + +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/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..ad4cd17e1 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -0,0 +1,68 @@ + + + + + + + + + + + {{page.name}} - 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. +

+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ff624c65b --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% 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 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% 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 new file mode 100644 index 000000000..5d47dc52f --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + {{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. +

+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py new file mode 100644 index 000000000..3e5c61a72 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -0,0 +1,69 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import view_config + +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') +def view_wiki(request): + return HTTPFound(location=request.route_url('view_page', + pagename='FrontPage')) + +@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: + return HTTPNotFound('No such page') + + def check(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(check, content) + edit_url = request.route_url('edit_page', pagename=pagename) + return dict(page=page, content=content, edit_url=edit_url) + +@view_config(route_name='add_page', renderer='templates/edit.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)) + 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'] + 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)) + return dict( + page=page, + save_url = request.route_url('edit_page', pagename=pagename), + ) -- cgit v1.2.3 From a47f067ca7491b797402604fd78a5a1132964f4d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 15 Nov 2015 05:36:13 -0800 Subject: wiki2/authorization.rst - progress on models and security from module to subpackage --- docs/tutorials/wiki2/authorization.rst | 51 ++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 1d810b05b..98e6110f3 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -18,22 +18,22 @@ require permission, instead of a default "403 Forbidden" page. We will implement the access control with the following steps: -* Add users and groups (``security.py``, a new module). -* Add an :term:`ACL` (``models.py`` and ``__init__.py``). +* Add 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.py``). + 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.py``). -* Add a login template (``login.pt``). +* 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 - (``views.py``). + (``views/default.py``). * Add a "Logout" link to be shown when logged in and viewing or editing a page - (``view.pt``, ``edit.pt``). + (``view.jinja2``, ``edit.jinja2``). Access control @@ -42,10 +42,10 @@ Access control Add users and groups ~~~~~~~~~~~~~~~~~~~~ -Create a new ``tutorial/tutorial/security.py`` module with the +Create a new ``tutorial/tutorial/security/default.py`` subpackage with the following content: -.. literalinclude:: src/authorization/tutorial/security.py +.. literalinclude:: src/authorization/tutorial/security/default.py :linenos: :language: python @@ -68,20 +68,21 @@ database, but here we use "dummy" data to represent user and groups sources. Add an ACL ~~~~~~~~~~ -Open ``tutorial/tutorial/models.py`` and add the following import -statement at the head: +Open ``tutorial/tutorial/models/mymodel.py`` and add the following import +statement just after the ``Base`` import at the top: -.. literalinclude:: src/authorization/tutorial/models.py - :lines: 1-4 +.. literalinclude:: src/authorization/tutorial/models/mymodel.py + :lines: 3-6 :linenos: + :lineno-start: 3 :language: python Add the following class definition at the end: -.. literalinclude:: src/authorization/tutorial/models.py - :lines: 33-37 +.. literalinclude:: src/authorization/tutorial/models/mymodel.py + :lines: 22-26 :linenos: - :lineno-start: 33 + :lineno-start: 22 :language: python We import :data:`~pyramid.security.Allow`, an action that means that @@ -90,9 +91,9 @@ permission is allowed, and :data:`~pyramid.security.Everyone`, a special :term:`ACE` entries that make up the ACL. The ACL is a list that needs to be named `__acl__` and be an attribute of a -class. We define an :term:`ACL` with two :term:`ACE` entries: the first entry -allows any user the `view` permission. The second entry allows the -``group:editors`` principal the `edit` permission. +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. 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 @@ -103,11 +104,11 @@ Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to our :term:`Configurator` constructor, that points to the class we created above: +.. TODO update the lines to include, linenos, lineno-start + .. literalinclude:: src/authorization/tutorial/__init__.py :lines: 24-25 - :linenos: :emphasize-lines: 2 - :lineno-start: 16 :language: python Only the highlighted line needs to be added. @@ -336,11 +337,11 @@ Our ``tutorial/tutorial/__init__.py`` will look like this when we're done: Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/models.py`` will look like this when we're done: +Our ``tutorial/tutorial/models/mymodel.py`` will look like this when we're done: -.. literalinclude:: src/authorization/tutorial/models.py +.. literalinclude:: src/authorization/tutorial/models/mymodel.py :linenos: - :emphasize-lines: 1-4,33-37 + :emphasize-lines: 3-6,22-26 :language: python Only the highlighted lines need to be added or edited. @@ -404,3 +405,5 @@ following URLs, checking that the result is as expected: 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. + +.. TODO update the lines to include in src/authorization/tutorial/__init__.py -- cgit v1.2.3 From a81ac719ce8a61305f20d05e10b3397b31ec8951 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 15 Nov 2015 23:46:30 -0800 Subject: wrap content with

--- docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 index 5d47dc52f..36bb96870 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -33,7 +33,7 @@

- {{ content|safe }} +

{{ content|safe }}

Edit this page -- cgit v1.2.3 From 4040cf7ef5a9843e25db69b3a17b3796f3a39fb8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 16 Nov 2015 00:17:20 -0800 Subject: - complete rewrite of wiki2/authorization.rst - add wiki2/src/authorization/ files - improve tag in views/tutorial/templates/edit.jinja2 --- docs/tutorials/wiki2/authorization.rst | 137 ++++++++-------- .../wiki2/src/authorization/development.ini | 4 +- .../wiki2/src/authorization/production.ini | 14 +- docs/tutorials/wiki2/src/authorization/setup.py | 2 +- .../wiki2/src/authorization/tutorial/__init__.py | 18 +-- .../wiki2/src/authorization/tutorial/models.py | 37 ----- .../src/authorization/tutorial/models/__init__.py | 7 + .../src/authorization/tutorial/models/meta.py | 46 ++++++ .../src/authorization/tutorial/models/mymodel.py | 26 +++ .../authorization/tutorial/scripts/initializedb.py | 25 +-- .../wiki2/src/authorization/tutorial/security.py | 7 - .../authorization/tutorial/security/__init__.py | 1 + .../src/authorization/tutorial/security/default.py | 7 + .../authorization/tutorial/static/theme.min.css | 2 +- .../authorization/tutorial/templates/edit.jinja2 | 73 +++++++++ .../src/authorization/tutorial/templates/edit.pt | 72 --------- .../authorization/tutorial/templates/layout.jinja2 | 66 ++++++++ .../authorization/tutorial/templates/login.jinja2 | 74 +++++++++ .../src/authorization/tutorial/templates/login.pt | 74 --------- .../tutorial/templates/mytemplate.jinja2 | 8 + .../authorization/tutorial/templates/mytemplate.pt | 66 -------- .../authorization/tutorial/templates/view.jinja2 | 71 +++++++++ .../src/authorization/tutorial/templates/view.pt | 72 --------- .../wiki2/src/authorization/tutorial/tests.py | 175 ++++++--------------- .../wiki2/src/authorization/tutorial/views.py | 124 --------------- .../src/authorization/tutorial/views/__init__.py | 0 .../src/authorization/tutorial/views/default.py | 120 ++++++++++++++ .../wiki2/src/views/tutorial/templates/edit.jinja2 | 2 +- 28 files changed, 648 insertions(+), 682 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security/default.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/default.py diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 98e6110f3..e40433497 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -104,10 +104,8 @@ Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to our :term:`Configurator` constructor, that points to the class we created above: -.. TODO update the lines to include, linenos, lineno-start - .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 24-25 + :lines: 13-14 :emphasize-lines: 2 :language: python @@ -128,18 +126,18 @@ Open ``tutorial/tutorial/__init__.py`` and add the highlighted import statements: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 1-7 + :lines: 1-5 :linenos: - :emphasize-lines: 2-3,7 + :emphasize-lines: 2-5 :language: python Now add those policies to the configuration: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 21-27 + :lines: 7-16 :linenos: - :lineno-start: 21 - :emphasize-lines: 1-3,6-7 + :lineno-start: 7 + :emphasize-lines: 4-6,9-10 :language: python Only the highlighted lines need to be added. @@ -152,47 +150,50 @@ ticket that may be included in the request. We are also enabling an 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 +machinery represented by this policy; it is required. The ``callback`` is the ``groupfinder()`` function that we created before. + Add permission declarations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/views.py`` and add a ``permission='edit'`` parameter -to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 60-61 +Open ``tutorial/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 :language: python -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 75-76 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 33-35 :emphasize-lines: 1-2 :language: python Only the highlighted lines, along with their preceding commas, need to be edited and added. -The result is that only users who possess the ``edit`` permission at the time -of the request may invoke those two views. +This allows anyone to invoke these two views. -Add a ``permission='view'`` parameter to the ``@view_config`` decorator for -``view_wiki()`` and ``view_page()`` as follows: +Add a ``permission='edit'`` parameter to the ``@view_config`` decorators for +``add_page()`` and ``edit_page()``: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 30-31 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 57-59 :emphasize-lines: 1-2 :language: python -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 36-37 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 72-74 :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 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. @@ -206,7 +207,7 @@ Go back to ``tutorial/tutorial/__init__.py`` and add these two routes as highlighted: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 30-33 + :lines: 20-23 :emphasize-lines: 2-3 :language: python @@ -214,7 +215,7 @@ highlighted: ``view_page`` route definition: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 33 + :lines: 23 :language: python This is because ``view_page``'s route definition uses a catch-all @@ -234,11 +235,11 @@ We'll also add a ``logout`` view callable to our application and provide a link to it. This view will clear the credentials of the logged in user and redirect back to the front page. -Add the following import statements to the head of -``tutorial/tutorial/views.py``: +Add the following import statements to ``tutorial/tutorial/views/default.py`` +after the import from ``pyramid.httpexceptions``: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 9-19 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 10-20 :emphasize-lines: 1-11 :language: python @@ -251,18 +252,18 @@ cookie. Now add the ``login`` and ``logout`` views at the end of the file: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 91-123 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 88-121 :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 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 + 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. @@ -270,36 +271,36 @@ The order of these two :term:`view configuration` decorators is unimportant. ``logout()`` is decorated with a ``@view_config`` decorator which associates it with the ``logout`` route. It will be invoked when we visit ``/logout``. -Add the ``login.pt`` Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add the ``login.jinja2`` template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create ``tutorial/tutorial/templates/login.pt`` with the following content: +Create ``tutorial/tutorial/templates/login.jinja2`` with the following content: -.. literalinclude:: src/authorization/tutorial/templates/login.pt +.. literalinclude:: src/authorization/tutorial/templates/login.jinja2 :language: html The above template is referenced in the login view that we just added in -``views.py``. +``views/default.py``. Return a ``logged_in`` flag to the renderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/views.py`` again. Add a ``logged_in`` parameter to -the return value of ``view_page()``, ``edit_page()``, and ``add_page()`` as -follows: +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.py - :lines: 57-58 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 54-55 :emphasize-lines: 1-2 :language: python -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 72-73 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 69-70 :emphasize-lines: 1-2 :language: python -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 85-89 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 82-86 :emphasize-lines: 3-4 :language: python @@ -311,19 +312,19 @@ the user is not authenticated, or a userid if the user is authenticated. Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/templates/edit.pt`` and -``tutorial/tutorial/templates/view.pt`` and add the following code as +Open ``tutorial/tutorial/templates/edit.jinja2`` and +``tutorial/tutorial/templates/view.jinja2`` and add the following code as indicated by the highlighted lines. -.. literalinclude:: src/authorization/tutorial/templates/edit.pt - :lines: 34-38 - :emphasize-lines: 3-5 +.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 + :lines: 34-40 + :emphasize-lines: 3-7 :language: html -The attribute ``tal:condition="logged_in"`` will make the element be included -when ``logged_in`` is any user id. The link will invoke the logout view. The -above element will not be included if ``logged_in`` is ``None``, such as when -a user is not authenticated. +The attribute ``logged_in`` will make the element be included when +``logged_in`` is any user id. The link will invoke the logout view. The above +element will not be included if ``logged_in`` is ``None``, such as when a user +is not authenticated. Reviewing our changes --------------------- @@ -332,7 +333,7 @@ Our ``tutorial/tutorial/__init__.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/__init__.py :linenos: - :emphasize-lines: 2-3,7,21-23,25-27,31-32 + :emphasize-lines: 2-3,5,10-12,14-16,21-22 :language: python Only the highlighted lines need to be added or edited. @@ -346,31 +347,31 @@ Our ``tutorial/tutorial/models/mymodel.py`` will look like this when we're done: Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/views.py`` will look like this when we're done: +Our ``tutorial/tutorial/views/default.py`` will look like this when we're done: -.. literalinclude:: src/authorization/tutorial/views.py +.. literalinclude:: src/authorization/tutorial/views/default.py :linenos: - :emphasize-lines: 9-11,14-19,25,31,37,58,61,73,76,88,91-117,119-123 + :emphasize-lines: 10-20,27-28,33-34,54-55,57-58,69-70,72-73,84-85,88-121 :language: python Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/templates/edit.pt`` template will look like this when +Our ``tutorial/tutorial/templates/edit.jinja2`` template will look like this when we're done: -.. literalinclude:: src/authorization/tutorial/templates/edit.pt +.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 :linenos: - :emphasize-lines: 36-38 + :emphasize-lines: 36-40 :language: html Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/templates/view.pt`` template will look like this when +Our ``tutorial/tutorial/templates/view.jinja2`` template will look like this when we're done: -.. literalinclude:: src/authorization/tutorial/templates/view.pt +.. literalinclude:: src/authorization/tutorial/templates/view.jinja2 :linenos: - :emphasize-lines: 36-38 + :emphasize-lines: 36-40 :language: html Only the highlighted lines need to be added or edited. @@ -405,5 +406,3 @@ following URLs, checking that the result is as expected: 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. - -.. TODO update the lines to include in src/authorization/tutorial/__init__.py diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index a9d53b296..99c4ff0fe 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index 4684d2f7a..97acfbd7d 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -16,7 +21,10 @@ use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -51,6 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index 09bd63d33..d4e5a4072 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'pyramid_tm', 'SQLAlchemy', diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 2ada42171..084fee19f 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -2,30 +2,20 @@ from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from sqlalchemy import engine_from_config - -from tutorial.security import groupfinder - -from .models import ( - DBSession, - Base, - ) - +from security.default import groupfinder def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, - root_factory='tutorial.models.RootFactory') + root_factory='tutorial.models.mymodel.RootFactory') config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) - config.include('pyramid_chameleon') + config.include('pyramid_jinja2') + config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models.py b/docs/tutorials/wiki2/src/authorization/tutorial/models.py deleted file mode 100644 index 4f7e1e024..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models.py +++ /dev/null @@ -1,37 +0,0 @@ -from pyramid.security import ( - Allow, - Everyone, - ) - -from sqlalchemy import ( - Column, - Integer, - Text, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) - - -class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py new file mode 100644 index 000000000..7b1c62867 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import Page # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py @@ -0,0 +1,46 @@ +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 + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py new file mode 100644 index 000000000..03e2f90ca --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py @@ -0,0 +1,26 @@ +from .meta import Base + +from pyramid.security import ( + Allow, + Everyone, + ) + +from sqlalchemy import ( + Column, + Integer, + Text, + ) + + +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 \ No newline at end of file diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py index 23a5f13f4..4aac4a848 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py @@ -2,36 +2,41 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, ) -from ..models import ( - DBSession, - Page, +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import Page def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s <config_uri>\n' + print('usage: %s <config_uri> [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] setup_logging(config_uri) settings = get_appsettings(config_uri) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py deleted file mode 100644 index d88c9c71f..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py +++ /dev/null @@ -1,7 +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/authorization/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py new file mode 100644 index 000000000..d88c9c71f --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py @@ -0,0 +1,7 @@ +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/authorization/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css index 2f924bcc5..0d25de5b6 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css +++ b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css @@ -1 +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{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file +@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/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..c4f3a2c93 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -0,0 +1,73 @@ +<!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 logged_in %} +

+ Logout +

+ {% endif %} +

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

+

You can return to the + FrontPage. +

+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt deleted file mode 100644 index ed355434d..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Logout -

-

- Editing Page Name Goes - Here -

-

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 new file mode 100644 index 000000000..ff624c65b --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..a80a2a165 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 @@ -0,0 +1,74 @@ + + + + + + + + + + + Login - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

+ + Login +
+ {{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt deleted file mode 100644 index 4a938e9bb..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - Login - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- - Login -
- -

-
- -
- - -
-
- - -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% 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/templates/mytemplate.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt deleted file mode 100644 index c9b0cec21..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

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

-
-
-
- -
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..a7afc66fc --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -0,0 +1,71 @@ + + + + + + + + + + + {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if logged_in %} +

+ 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. +

+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt deleted file mode 100644 index 02cb8e73b..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Logout -

-
- Page text goes here. -
-

- - Edit this page - -

-

- Viewing - Page Name Goes Here -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py index 9f01d2da5..b947e3bb1 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py @@ -3,144 +3,63 @@ import transaction from pyramid import testing -def _initTestingDB(): - from sqlalchemy import create_engine - from tutorial.models import ( - DBSession, - Page, - Base - ) - engine = create_engine('sqlite://') - Base.metadata.create_all(engine) - DBSession.configure(bind=engine) - with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) - return DBSession - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - -class ViewWikiTests(unittest.TestCase): + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - self.session = _initTestingDB() + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, + ) - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') + self.session = get_session(transaction.manager, dbmaker) -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '
\n' - '

Hello ' - 'CruelWorld ' - '' - 'IDoExist' - '

\n
\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - - -class AddPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + from .models.meta import Base - def tearDown(self): - self.session.remove() testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - -class EditPageTests(unittest.TestCase): def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models.mymodel import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], - 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py deleted file mode 100644 index e954d5a31..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ /dev/null @@ -1,124 +0,0 @@ -import re -from docutils.core import publish_parts - -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) - -from pyramid.view import ( - view_config, - forbidden_view_config, - ) - -from pyramid.security import ( - remember, - forget, - ) - -from .security import USERS - -from .models import ( - DBSession, - Page, - ) - - -# regular expression used to find WikiWords -wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - -@view_config(route_name='view_wiki', - permission='view') -def view_wiki(request): - return HTTPFound(location = request.route_url('view_page', - pagename='FrontPage')) - -@view_config(route_name='view_page', renderer='templates/view.pt', - permission='view') -def view_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).first() - if page is None: - return HTTPNotFound('No such page') - - def check(match): - word = match.group(1) - exists = DBSession.query(Page).filter_by(name=word).all() - if exists: - view_url = request.route_url('view_page', pagename=word) - return '%s' % (view_url, word) - else: - add_url = request.route_url('add_page', pagename=word) - return '%s' % (add_url, word) - - content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=request.authenticated_userid) - -@view_config(route_name='add_page', renderer='templates/edit.pt', - permission='edit') -def add_page(request): - pagename = request.matchdict['pagename'] - if 'form.submitted' in request.params: - body = request.params['body'] - page = Page(name=pagename, data=body) - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url, - logged_in=request.authenticated_userid) - -@view_config(route_name='edit_page', renderer='templates/edit.pt', - permission='edit') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - return dict( - page=page, - save_url=request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid - ) - -@view_config(route_name='login', renderer='templates/login.pt') -@forbidden_view_config(renderer='templates/login.pt') -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -@view_config(route_name='logout') -def logout(request): - headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py new file mode 100644 index 000000000..f35f041a4 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -0,0 +1,120 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import ( + view_config, + forbidden_view_config, + ) + +from pyramid.security import ( + remember, + forget, + ) + +from ..security.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') +def view_wiki(request): + return HTTPFound(location=request.route_url('view_page', + pagename='FrontPage')) + +@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') + + def check(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(check, content) + edit_url = request.route_url('edit_page', pagename=pagename) + return dict(page=page, content=content, edit_url=edit_url, + logged_in=request.authenticated_userid) + +@view_config(route_name='add_page', renderer='templates/edit.jinja2', + permission='edit') +def add_page(request): + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + request.dbsession.add(page) + return HTTPFound(location = request.route_url('view_page', + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) + page = Page(name='', data='') + return dict(page=page, save_url=save_url, + logged_in=request.authenticated_userid) + +@view_config(route_name='edit_page', renderer='templates/edit.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)) + return dict( + page=page, + save_url = request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid + ) + +@view_config(route_name='login', renderer='templates/login.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.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location = request.route_url('view_wiki'), + headers = headers) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 index ad4cd17e1..b3aadfc2e 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -8,7 +8,7 @@ - {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) -- cgit v1.2.3 From 6fcd98abe5bf951eb993b1a6b24126a5681221a5 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 16 Nov 2015 00:49:58 -0800 Subject: - update wiki2/design.rst to use jinja templates - minor grammar - .rst fixes - rewrap to 79 columns --- docs/tutorials/wiki2/design.rst | 180 +++++++++++++++++++--------------------- 1 file changed, 87 insertions(+), 93 deletions(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index e9f361e7d..8e3bb4c13 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -1,10 +1,9 @@ -========== +====== Design -========== +====== -Following is a quick overview of the design of our wiki application, to help -us understand the changes that we will be making as we work through the -tutorial. +Following is a quick overview of the design of our wiki application to help us +understand the changes that we will be making as we work through the tutorial. Overall ------- @@ -17,17 +16,17 @@ Python module. We will add this module in the dependency list on the project Models ------ -We'll be using a SQLite database to hold our wiki data, and we'll be using +We'll be using an SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. Within the database, we define a single table named `pages`, whose elements will store the wiki pages. There are two columns: `name` and `data`. -URLs like ``/PageName`` will try to find an element in -the table that has a corresponding name. +URLs like ``/PageName`` will try to find an element in the 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`. +To add a page to the wiki, a new row is created and the text is stored in +`data`. A page named ``FrontPage`` containing the text *This is the front page*, will be created when the storage is initialized, and will be used as the wiki home @@ -36,16 +35,14 @@ page. Views ----- -There will be three views to handle the normal operations of adding, -editing, and viewing wiki pages, plus one view for the wiki front page. -Two templates will be used, one for viewing, and one for both adding -and editing wiki pages. +There will be three views to handle the normal operations of adding, editing, +and viewing wiki pages, plus one view for the wiki front page. Two templates +will be used, one for viewing, and one for both adding and editing wiki pages. -The default templating systems in :app:`Pyramid` are -:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of -:term:`ZPT`, which is an XML-based templating language. Mako is a -non-XML-based templating language. Because we had to pick one, -we chose Chameleon for this tutorial. +As of version 1.5 :app:`Pyramid` no longer ships with templating systems. In +this tutorial, we will use :term:`Jinja2`. Jinja2 is a modern and +designer-friendly templating language for Python, modeled after Django's +templates. Security -------- @@ -53,14 +50,14 @@ 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. +- 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. +- 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.py`` file. +- ``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`: @@ -76,75 +73,72 @@ use to do this are below. - 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. +Two additional views and one template will handle the login and logout tasks. Summary ------- -The URL, actions, template and permission associated to each view are -listed in the following table: - -+----------------------+-----------------------+-------------+------------+------------+ -| URL | Action | View | Template | Permission | -| | | | | | -+======================+=======================+=============+============+============+ -| / | Redirect to | view_wiki | | | -| | /FrontPage | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /PageName | Display existing | view_page | view.pt | view | -| | page [2]_ | [1]_ | | | -| | | | | | -| | | | | | -| | | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /PageName/edit_page | Display edit form | edit_page | edit.pt | edit | -| | with existing | | | | -| | content. | | | | -| | | | | | -| | If the form was | | | | -| | submitted, redirect | | | | -| | to /PageName | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /add_page/PageName | Create the page | add_page | edit.pt | edit | -| | *PageName* in | | | | -| | storage, display | | | | -| | the edit form | | | | -| | without content. | | | | -| | | | | | -| | If the form was | | | | -| | submitted, | | | | -| | redirect to | | | | -| | /PageName | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /login | Display login form, | login | login.pt | | -| | Forbidden [3]_ | | | | -| | | | | | -| | If the form was | | | | -| | submitted, | | | | -| | authenticate. | | | | -| | | | | | -| | - If authentication | | | | -| | succeeds, | | | | -| | redirect to the | | | | -| | page that we | | | | -| | came from. | | | | -| | | | | | -| | - If authentication | | | | -| | fails, display | | | | -| | login form with | | | | -| | "login failed" | | | | -| | message. | | | | -| | | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /logout | Redirect to | logout | | | -| | /FrontPage | | | | -+----------------------+-----------------------+-------------+------------+------------+ - -.. [1] This is the default view for a Page context - when there is no view name. -.. [2] Pyramid will return a default 404 Not Found page - if the page *PageName* does not exist yet. -.. [3] ``pyramid.exceptions.Forbidden`` is reached when a - user tries to invoke a view that is - not authorized by the authorization policy. +The URL, actions, template, and permission associated to each view are listed +in the following table: + ++----------------------+-----------------------+-------------+----------------+------------+ +| URL | Action | View | Template | Permission | +| | | | | | ++======================+=======================+=============+================+============+ +| / | Redirect to | view_wiki | | | +| | /FrontPage | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /PageName | Display existing | view_page | view.jinja2 | view | +| | page [2]_ | [1]_ | | | +| | | | | | +| | | | | | +| | | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /PageName/edit_page | Display edit form | edit_page | edit.jinja2 | edit | +| | with existing | | | | +| | content. | | | | +| | | | | | +| | If the form was | | | | +| | submitted, redirect | | | | +| | to /PageName | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /add_page/PageName | Create the page | add_page | edit.jinja2 | edit | +| | *PageName* in | | | | +| | storage, display | | | | +| | the edit form | | | | +| | without content. | | | | +| | | | | | +| | If the form was | | | | +| | submitted, | | | | +| | redirect to | | | | +| | /PageName | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /login | Display login form, | login | login.jinja2 | | +| | Forbidden [3]_ | | | | +| | | | | | +| | If the form was | | | | +| | submitted, | | | | +| | authenticate. | | | | +| | | | | | +| | - If authentication | | | | +| | succeeds, | | | | +| | redirect to the | | | | +| | page that we | | | | +| | came from. | | | | +| | | | | | +| | - If authentication | | | | +| | fails, display | | | | +| | login form with | | | | +| | "login failed" | | | | +| | message. | | | | +| | | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /logout | Redirect to | logout | | | +| | /FrontPage | | | | ++----------------------+-----------------------+-------------+----------------+------------+ + +.. [1] This is the default view for a Page context when there is no view name. +.. [2] Pyramid will return a default 404 Not Found page if the page *PageName* + does not exist yet. +.. [3] ``pyramid.exceptions.Forbidden`` is reached when a user tries to invoke + a view that is not authorized by the authorization policy. -- cgit v1.2.3 From fe4465b91296ca7d41c4f7c8006cbd8d50851a09 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 20 Nov 2015 17:48:38 -0800 Subject: wiki2/tests - commit progress --- docs/tutorials/wiki2/tests.rst | 77 +++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 9db95334a..64025421f 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -2,25 +2,38 @@ Adding Tests ============ -We will now add tests for the models and the views and a few functional tests -in ``tests.py``. Tests ensure that an application works, and that it -continues to work when changes are made in the future. +We will now add tests for the models and views as well as a few functional +tests in a new ``tests`` subpackage. Tests ensure that an application works, +and that it continues to work when changes are made in the future. + +The file ``tests.py`` was generated as part of the ``alchemy`` scaffold, but it +is a common practice to put tests into a ``tests`` subpackage, especially as +projects grow in size and complexity. Each module in the test subpackage +should contain tests for its corresponding module in our application. Each +corresponding pair of modules should have the same names, except the test +module should have the prefix "test_". + +We will move parts of ``tests.py`` into appropriate new files in the ``tests`` +subpackage, and add several new tests. + Test the models =============== -To test the model class ``Page`` we'll add a new ``PageModelTests`` class to -our ``tests.py`` file that was generated as part of the ``alchemy`` scaffold. +To test the model class ``Page``, we'll add a new ``PageModelTests`` class to +a new file ``tests/test_models.py`` + Test the views ============== -We'll modify our ``tests.py`` file, adding tests for each view function we -added previously. As a result, we'll *delete* the ``ViewTests`` class that -the ``alchemy`` scaffold provided, and add four other test classes: -``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``. -These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` -views. +We'll create a new ``tests/test_views.py`` file, adding tests for each view +function we previously added to our application. As a result, we'll *delete* +the ``ViewTests`` class that the ``alchemy`` scaffold provided, and add four +other test classes: ``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and +``EditPageTests``. These test the ``view_wiki``, ``view_page``, ``add_page``, +and ``edit_page`` views. + Functional tests ================ @@ -30,23 +43,41 @@ tested in the unit tests, like logging in, logging out, checking that the ``viewer`` user cannot add or edit pages, but the ``editor`` user can, and so on. -View the results of all our edits to ``tests.py`` -================================================= -Open the ``tutorial/tests.py`` module, and edit it such that it appears as +View the results of all our edits to ``tests`` subpackage +========================================================= + +Open ``tutorial/tests/test_models.py``, and edit it such that it appears as follows: -.. literalinclude:: src/tests/tutorial/tests.py +.. literalinclude:: src/tests/tutorial/tests/test_models.py :linenos: :language: python +Open ``tutorial/tests/test_views.py``, and edit it such that it appears as +follows: + +.. literalinclude:: src/tests/tutorial/tests/test_views.py + :linenos: + :language: python + +Open ``tutorial/tests/test_.py``, and edit it such that it appears as +follows: + +.. literalinclude:: src/tests/tutorial/tests/test_.py + :linenos: + :language: python + + Running the tests ================= We can run these tests by using ``setup.py test`` in the same way we did in -:ref:`running_tests`. However, first we must edit our ``setup.py`` to -include a dependency on WebTest, which we've used in our ``tests.py``. -Change the ``requires`` list in ``setup.py`` to include ``WebTest``. +: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: @@ -56,12 +87,11 @@ Change the ``requires`` list in ``setup.py`` to include ``WebTest``. 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: +our shell's current working directory is the "tutorial" distribution directory: On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py develop @@ -71,12 +101,11 @@ On Windows: c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -Once that command has completed successfully, we can run the tests -themselves: +Once that command has completed successfully, we can run the tests themselves: On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py test -q -- cgit v1.2.3 From 9b12c01168cb756ec36351d7414cad95e87f6581 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Tue, 1 Dec 2015 23:18:00 -0700 Subject: Add documentation why we add a naming convention --- docs/quick_tour/sqla_demo/sqla_demo/models/meta.py | 3 +++ pyramid/scaffolds/alchemy/+package+/models/meta.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/pyramid/scaffolds/alchemy/+package+/models/meta.py b/pyramid/scaffolds/alchemy/+package+/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/meta.py +++ b/pyramid/scaffolds/alchemy/+package+/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", -- cgit v1.2.3 From f10fb24a3663cb070b28b8020fde957d16563400 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 2 Dec 2015 04:18:10 -0800 Subject: - rewrite wiki2/tests.rst (removed an obsolete reference to testing models, per 2885a7b96545c037109d7999319f74869a640050) - add wiki2/src/tests/ files. special thanks to @ppaez --- docs/tutorials/wiki2/src/tests/development.ini | 4 +- docs/tutorials/wiki2/src/tests/production.ini | 14 +- docs/tutorials/wiki2/src/tests/setup.py | 4 +- .../tutorials/wiki2/src/tests/tutorial/__init__.py | 18 +- docs/tutorials/wiki2/src/tests/tutorial/models.py | 37 ---- .../wiki2/src/tests/tutorial/models/__init__.py | 7 + .../wiki2/src/tests/tutorial/models/meta.py | 46 ++++ .../wiki2/src/tests/tutorial/models/mymodel.py | 26 +++ .../src/tests/tutorial/scripts/initializedb.py | 25 ++- .../tutorials/wiki2/src/tests/tutorial/security.py | 7 - .../wiki2/src/tests/tutorial/security/__init__.py | 1 + .../wiki2/src/tests/tutorial/security/default.py | 7 + .../wiki2/src/tests/tutorial/static/theme.min.css | 2 +- .../wiki2/src/tests/tutorial/templates/edit.jinja2 | 73 +++++++ .../wiki2/src/tests/tutorial/templates/edit.pt | 74 ------- .../src/tests/tutorial/templates/layout.jinja2 | 66 ++++++ .../src/tests/tutorial/templates/login.jinja2 | 74 +++++++ .../wiki2/src/tests/tutorial/templates/login.pt | 54 ----- .../src/tests/tutorial/templates/mytemplate.jinja2 | 8 + .../src/tests/tutorial/templates/mytemplate.pt | 66 ------ .../wiki2/src/tests/tutorial/templates/view.jinja2 | 71 +++++++ .../wiki2/src/tests/tutorial/templates/view.pt | 74 ------- docs/tutorials/wiki2/src/tests/tutorial/tests.py | 235 --------------------- .../wiki2/src/tests/tutorial/tests/__init__.py | 1 + .../src/tests/tutorial/tests/test_functional.py | 141 +++++++++++++ .../wiki2/src/tests/tutorial/tests/test_views.py | 168 +++++++++++++++ docs/tutorials/wiki2/src/tests/tutorial/views.py | 123 ----------- .../wiki2/src/tests/tutorial/views/__init__.py | 0 .../wiki2/src/tests/tutorial/views/default.py | 120 +++++++++++ docs/tutorials/wiki2/tests.rst | 26 +-- 30 files changed, 851 insertions(+), 721 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security/default.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/tests.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/default.py diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index a9d53b296..99c4ff0fe 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index 4684d2f7a..97acfbd7d 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -16,7 +21,10 @@ use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -51,6 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index d8486e462..f640b4399 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'pyramid_tm', 'SQLAlchemy', @@ -18,7 +18,7 @@ requires = [ 'zope.sqlalchemy', 'waitress', 'docutils', - 'WebTest', # add this + '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 cee89184b..084fee19f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -2,30 +2,20 @@ from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from sqlalchemy import engine_from_config - -from tutorial.security import groupfinder - -from .models import ( - DBSession, - Base, - ) - +from security.default import groupfinder def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, - root_factory='tutorial.models.RootFactory') - config.include('pyramid_chameleon') + root_factory='tutorial.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.py b/docs/tutorials/wiki2/src/tests/tutorial/models.py deleted file mode 100644 index 4f7e1e024..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/models.py +++ /dev/null @@ -1,37 +0,0 @@ -from pyramid.security import ( - Allow, - Everyone, - ) - -from sqlalchemy import ( - Column, - Integer, - Text, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) - - -class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py new file mode 100644 index 000000000..7b1c62867 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import Page # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py @@ -0,0 +1,46 @@ +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 + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py new file mode 100644 index 000000000..03e2f90ca --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py @@ -0,0 +1,26 @@ +from .meta import Base + +from pyramid.security import ( + Allow, + Everyone, + ) + +from sqlalchemy import ( + Column, + Integer, + Text, + ) + + +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 \ No newline at end of file diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py index 23a5f13f4..4aac4a848 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -2,36 +2,41 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, ) -from ..models import ( - DBSession, - Page, +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import Page def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s \n' + print('usage: %s [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] setup_logging(config_uri) settings = get_appsettings(config_uri) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py deleted file mode 100644 index d88c9c71f..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/security.py +++ /dev/null @@ -1,7 +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/security/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py new file mode 100644 index 000000000..d88c9c71f --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py @@ -0,0 +1,7 @@ +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/static/theme.min.css b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css index 2f924bcc5..0d25de5b6 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css +++ b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css @@ -1 +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{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file +@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/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..c4f3a2c93 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -0,0 +1,73 @@ + + + + + + + + + + + Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if logged_in %} +

+ Logout +

+ {% endif %} +

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

+

You can return to the + FrontPage. +

+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt deleted file mode 100644 index 50e55c850..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Editing Page Name Goes - Here -

-

You can return to the - FrontPage. -

-

- - Logout - -

-
-
- -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - 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..ff624c65b --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..a80a2a165 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 @@ -0,0 +1,74 @@ + + + + + + + + + + + Login - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

+ + Login +
+ {{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt deleted file mode 100644 index 5f8e9b98c..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt +++ /dev/null @@ -1,54 +0,0 @@ - - - - Login - Pyramid tutorial wiki (based on TurboGears - 20-Minute Wiki) - - - - - - - - -
-
-
-
- pyramid -
-
-
-
-
-
- Login
- -
- -
-
-
-
-
- -
-
- -
-
-
-
- - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% 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/templates/mytemplate.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt deleted file mode 100644 index c9b0cec21..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

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

-
-
-
- -
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..a7afc66fc --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -0,0 +1,71 @@ + + + + + + + + + + + {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if logged_in %} +

+ 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. +

+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt deleted file mode 100644 index 4e5772de0..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-
- Page text goes here. -
-

- - Edit this page - -

-

- Viewing - Page Name Goes Here -

-

You can return to the - FrontPage. -

-

- - Logout - -

-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py deleted file mode 100644 index c50e05b6d..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests.py +++ /dev/null @@ -1,235 +0,0 @@ -import unittest -import transaction - -from pyramid import testing - - -def _initTestingDB(): - from sqlalchemy import create_engine - from tutorial.models import ( - DBSession, - Page, - Base - ) - engine = create_engine('sqlite://') - Base.metadata.create_all(engine) - DBSession.configure(bind=engine) - with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) - return DBSession - - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - - -class ViewWikiTests(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) - - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') - - -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '
\n' - '

Hello ' - 'CruelWorld ' - '' - 'IDoExist' - '

\n
\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - - -class AddPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - - -class EditPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], - 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') - - -class FunctionalTests(unittest.TestCase): - - viewer_login = '/login?login=viewer&password=viewer' \ - '&came_from=FrontPage&form.submitted=Login' - viewer_wrong_login = '/login?login=viewer&password=incorrect' \ - '&came_from=FrontPage&form.submitted=Login' - editor_login = '/login?login=editor&password=editor' \ - '&came_from=FrontPage&form.submitted=Login' - - def setUp(self): - from tutorial import main - settings = { 'sqlalchemy.url': 'sqlite://'} - app = main({}, **settings) - from webtest import TestApp - self.testapp = TestApp(app) - _initTestingDB() - - def tearDown(self): - del self.testapp - from tutorial.models import DBSession - DBSession.remove() - - def test_root(self): - res = self.testapp.get('/', status=302) - self.assertEqual(res.location, 'http://localhost/FrontPage') - - def test_FrontPage(self): - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue(b'FrontPage' in res.body) - - def test_unexisting_page(self): - self.testapp.get('/SomePage', status=404) - - def test_successful_log_in(self): - res = self.testapp.get(self.viewer_login, status=302) - self.assertEqual(res.location, 'http://localhost/FrontPage') - - def test_failed_log_in(self): - res = self.testapp.get(self.viewer_wrong_login, status=200) - self.assertTrue(b'login' in res.body) - - def test_logout_link_present_when_logged_in(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue(b'Logout' in res.body) - - def test_logout_link_not_present_after_logged_out(self): - self.testapp.get(self.viewer_login, status=302) - self.testapp.get('/FrontPage', status=200) - res = self.testapp.get('/logout', status=302) - self.assertTrue(b'Logout' not in res.body) - - def test_anonymous_user_cannot_edit(self): - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue(b'Login' in res.body) - - def test_anonymous_user_cannot_add(self): - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue(b'Login' in res.body) - - def test_viewer_user_cannot_edit(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue(b'Login' in res.body) - - def test_viewer_user_cannot_add(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue(b'Login' in res.body) - - def test_editors_member_user_can_edit(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue(b'Editing' in res.body) - - def test_editors_member_user_can_add(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue(b'Editing' in res.body) - - def test_editors_member_user_can_view(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue(b'FrontPage' in res.body) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py new file mode 100644 index 000000000..339c60bc2 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -0,0 +1,141 @@ +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): + + 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' + + engine = None + + @staticmethod + def setup_database(): + import transaction + from tutorial.models.mymodel import Page + from tutorial.models.meta import ( + Base, + ) + import tutorial.models.meta + + + 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_session(transaction_manager, dbmaker): + dbsession = get_session(transaction_manager, dbmaker) + 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): + global engine + engine = get_engine(settings) + return engine + + get_session = tutorial.models.meta.get_session + tutorial.models.meta.get_session = wrap_get_session + + get_engine = tutorial.models.meta.get_engine + tutorial.models.meta.get_engine = wrap_get_engine + + @classmethod + def setUpClass(cls): + cls.setup_database() + + from webtest import TestApp + from tutorial import main + settings = {'sqlalchemy.url': 'sqlite://'} + app = main({}, **settings) + cls.testapp = TestApp(app) + + @classmethod + def tearDownClass(cls): + from tutorial.models.meta import Base + Base.metadata.drop_all(engine) + + def tearDown(self): + import transaction + transaction.abort() + + def test_root(self): + res = self.testapp.get('/', status=302) + self.assertEqual(res.location, 'http://localhost/FrontPage') + + def test_FrontPage(self): + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'FrontPage' in res.body) + + def test_unexisting_page(self): + self.testapp.get('/SomePage', status=404) + + def test_successful_log_in(self): + res = self.testapp.get(self.viewer_login, status=302) + self.assertEqual(res.location, 'http://localhost/FrontPage') + + def test_failed_log_in(self): + res = self.testapp.get(self.viewer_wrong_login, status=200) + self.assertTrue(b'login' in res.body) + + def test_logout_link_present_when_logged_in(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'Logout' in res.body) + + def test_logout_link_not_present_after_logged_out(self): + self.testapp.get(self.viewer_login, status=302) + self.testapp.get('/FrontPage', status=200) + res = self.testapp.get('/logout', status=302) + self.assertTrue(b'Logout' not in res.body) + + def test_anonymous_user_cannot_edit(self): + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue(b'Login' in res.body) + + def test_anonymous_user_cannot_add(self): + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Login' in res.body) + + def test_viewer_user_cannot_edit(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue(b'Login' in res.body) + + def test_viewer_user_cannot_add(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Login' in res.body) + + def test_editors_member_user_can_edit(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_editors_member_user_can_add(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_editors_member_user_can_view(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'FrontPage' in res.body) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py new file mode 100644 index 000000000..d70311e38 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -0,0 +1,168 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +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 BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('..models.meta') + _register_routes(self.config) + settings = self.config.get_settings() + + 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) + + self.init_database() + + def init_database(self): + from ..models.meta import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + testing.tearDown() + transaction.abort() + + +class ViewWikiTests(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + _register_routes(self.config) + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views.default import view_wiki + return view_wiki(request) + + def test_it(self): + request = testing.DummyRequest() + response = self._callFUT(request) + self.assertEqual(response.location, 'http://example.com/FrontPage') + + +class ViewPageTests(BaseTest): + def 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) + + def test_it(self): + # add a page to the db + from ..models.mymodel import Page + page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') + self.session.add(page) + + # create a request asking for the page we've created + request = dummy_request(self.session) + request.matchdict['pagename'] = 'IDoExist' + + # call the view we're testing and check its behavior + info = self._callFUT(request) + self.assertEqual(info['page'], page) + self.assertEqual( + info['content'], + '
\n' + '

Hello ' + 'CruelWorld ' + '' + 'IDoExist' + '

\n
\n') + self.assertEqual(info['edit_url'], + 'http://example.com/IDoExist/edit_page') + + +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) + + def test_it_notsubmitted(self): + request = dummy_request(self.session) + request.matchdict = {'pagename': 'AnotherPage'} + info = self._callFUT(request) + self.assertEqual(info['page'].data, '') + self.assertEqual(info['save_url'], + 'http://example.com/add_page/AnotherPage') + + def test_it_submitted(self): + from ..models.mymodel import Page + request = testing.DummyRequest({'form.submitted': True, + 'body': 'Hello yo!'}, + dbsession=self.session) + request.matchdict = {'pagename': 'AnotherPage'} + self._callFUT(request) + page = self.session.query(Page).filter_by(name='AnotherPage').one() + self.assertEqual(page.data, 'Hello yo!') + + +class EditPageTests(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) + + def test_it_notsubmitted(self): + from ..models.mymodel import Page + request = dummy_request(self.session) + request.matchdict = {'pagename': 'abc'} + page = Page(name='abc', data='hello') + self.session.add(page) + info = self._callFUT(request) + self.assertEqual(info['page'], page) + self.assertEqual(info['save_url'], + 'http://example.com/abc/edit_page') + + def test_it_submitted(self): + from ..models.mymodel import Page + 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) + response = self._callFUT(request) + self.assertEqual(response.location, 'http://example.com/abc') + self.assertEqual(page.data, 'Hello yo!') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py deleted file mode 100644 index 41bea4785..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ /dev/null @@ -1,123 +0,0 @@ -import re -from docutils.core import publish_parts - -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) - -from pyramid.view import ( - view_config, - forbidden_view_config, - ) - -from pyramid.security import ( - remember, - forget, - ) - -from .security import USERS - -from .models import ( - DBSession, - Page, - ) - - -# regular expression used to find WikiWords -wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - -@view_config(route_name='view_wiki', - permission='view') -def view_wiki(request): - return HTTPFound(location = request.route_url('view_page', - pagename='FrontPage')) - -@view_config(route_name='view_page', renderer='templates/view.pt', - permission='view') -def view_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).first() - if page is None: - return HTTPNotFound('No such page') - - def check(match): - word = match.group(1) - exists = DBSession.query(Page).filter_by(name=word).all() - if exists: - view_url = request.route_url('view_page', pagename=word) - return '%s' % (view_url, word) - else: - add_url = request.route_url('add_page', pagename=word) - return '%s' % (add_url, word) - - content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=request.authenticated_userid) - -@view_config(route_name='add_page', renderer='templates/edit.pt', - permission='edit') -def add_page(request): - pagename = request.matchdict['pagename'] - if 'form.submitted' in request.params: - body = request.params['body'] - page = Page(name=pagename, data=body) - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url, - logged_in=request.authenticated_userid) - -@view_config(route_name='edit_page', renderer='templates/edit.pt', - permission='edit') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - return dict( - page=page, - save_url=request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid - ) - -@view_config(route_name='login', renderer='templates/login.pt') -@forbidden_view_config(renderer='templates/login.pt') -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -@view_config(route_name='logout') -def logout(request): - headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py new file mode 100644 index 000000000..f35f041a4 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -0,0 +1,120 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import ( + view_config, + forbidden_view_config, + ) + +from pyramid.security import ( + remember, + forget, + ) + +from ..security.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') +def view_wiki(request): + return HTTPFound(location=request.route_url('view_page', + pagename='FrontPage')) + +@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') + + def check(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(check, content) + edit_url = request.route_url('edit_page', pagename=pagename) + return dict(page=page, content=content, edit_url=edit_url, + logged_in=request.authenticated_userid) + +@view_config(route_name='add_page', renderer='templates/edit.jinja2', + permission='edit') +def add_page(request): + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + request.dbsession.add(page) + return HTTPFound(location = request.route_url('view_page', + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) + page = Page(name='', data='') + return dict(page=page, save_url=save_url, + logged_in=request.authenticated_userid) + +@view_config(route_name='edit_page', renderer='templates/edit.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)) + return dict( + page=page, + save_url = request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid + ) + +@view_config(route_name='login', renderer='templates/login.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.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location = request.route_url('view_wiki'), + headers = headers) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 64025421f..fe3fdaf2c 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -11,17 +11,12 @@ is a common practice to put tests into a ``tests`` subpackage, especially as projects grow in size and complexity. Each module in the test subpackage should contain tests for its corresponding module in our application. Each corresponding pair of modules should have the same names, except the test -module should have the prefix "test_". +module should have the prefix ``test_``. We will move parts of ``tests.py`` into appropriate new files in the ``tests`` subpackage, and add several new tests. - -Test the models -=============== - -To test the model class ``Page``, we'll add a new ``PageModelTests`` class to -a new file ``tests/test_models.py`` +Start by creating a new directory and a new empty file ``tests/__init__.py``. Test the views @@ -47,13 +42,6 @@ can, and so on. View the results of all our edits to ``tests`` subpackage ========================================================= -Open ``tutorial/tests/test_models.py``, and edit it such that it appears as -follows: - -.. literalinclude:: src/tests/tutorial/tests/test_models.py - :linenos: - :language: python - Open ``tutorial/tests/test_views.py``, and edit it such that it appears as follows: @@ -61,10 +49,10 @@ follows: :linenos: :language: python -Open ``tutorial/tests/test_.py``, and edit it such that it appears as +Open ``tutorial/tests/test_functional.py``, and edit it such that it appears as follows: -.. literalinclude:: src/tests/tutorial/tests/test_.py +.. literalinclude:: src/tests/tutorial/tests/test_functional.py :linenos: :language: python @@ -119,8 +107,10 @@ The expected result should look like the following: .. code-block:: text - ...................... + .................... ---------------------------------------------------------------------- - Ran 21 tests in 2.700s + Ran 20 tests in 0.524s OK + + Process finished with exit code 0 -- cgit v1.2.3 From 022a6e0f05b72c679aada6b3c9313c4fd5b31b80 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 2 Dec 2015 04:30:47 -0800 Subject: - add comment to NAMING_CONVENTION per 9b12c01168cb756ec36351d7414cad95e87f6581 --- docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py | 3 +++ docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py | 3 +++ docs/tutorials/wiki2/src/models/tutorial/models/meta.py | 3 +++ docs/tutorials/wiki2/src/tests/tutorial/models/meta.py | 3 +++ docs/tutorials/wiki2/src/views/tutorial/models/meta.py | 3 +++ 5 files changed, 15 insertions(+) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", -- cgit v1.2.3 From 86d90fb8c22a50e68077542d8f3f0f488e72a5d8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 2 Dec 2015 04:38:58 -0800 Subject: - adjust linenos for NAMING_CONVENTION comment --- docs/tutorials/wiki2/basiclayout.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 1426e55a5..e3d0a0a3c 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -211,8 +211,8 @@ will inherit from the ``Base`` class so they can be associated with our particular database connection. .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :lines: 15-16 - :lineno-start: 15 + :lines: 18-19 + :lineno-start: 18 :linenos: :language: py @@ -221,7 +221,7 @@ configures various database settings by calling subsequently defined functions. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: includeme - :lineno-start: 19 + :lineno-start: 22 :linenos: :language: py @@ -232,7 +232,7 @@ 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: 32 + :lineno-start: 35 :linenos: :language: py @@ -243,7 +243,7 @@ URI, something like ``sqlite://``. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_engine - :lineno-start: 39 + :lineno-start: 42 :linenos: :language: py @@ -254,7 +254,7 @@ creating a session with the database engine. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_dbmaker - :lineno-start: 43 + :lineno-start: 46 :linenos: :language: py -- cgit v1.2.3 From 690fbb9dc7a1797fa81c217182766b150fc649b6 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 3 Dec 2015 21:22:53 -0800 Subject: - add updated tutorial and documentation files due to scaffold enhancements --- docs/whatsnew-1.6.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/whatsnew-1.6.rst b/docs/whatsnew-1.6.rst index b99ebeec4..141a58f78 100644 --- a/docs/whatsnew-1.6.rst +++ b/docs/whatsnew-1.6.rst @@ -130,11 +130,11 @@ Feature Additions extensions by going through Pyramid's router. See https://github.com/Pylons/pyramid/pull/1581 - - Make it possible to subclass ``pyramid.request.Request`` and also use ``pyramid.request.Request.add_request.method``. See https://github.com/Pylons/pyramid/issues/1529 + Deprecations ------------ @@ -151,13 +151,14 @@ Scaffolding Enhancements debugging. See https://github.com/Pylons/pyramid/pull/1326 - Update scaffold generating machinery to return the version of pyramid and - pyramid docs for use in scaffolds. Updated ``starter``, ``alchemy`` and + pyramid docs for use in scaffolds. Updated ``starter``, ``alchemy``, and ``zodb`` templates to have links to correctly versioned documentation and reflect which pyramid was used to generate the scaffold. - Removed non-ascii copyright symbol from templates, as this was causing the scaffolds to fail for project generation. + Documentation Enhancements -------------------------- @@ -168,3 +169,8 @@ Documentation Enhancements - Improve and clarify the documentation on what Pyramid defines as a ``principal`` and a ``userid`` in its security APIs. See https://github.com/Pylons/pyramid/pull/1399 + +- Updated the Quick Tour and SQLAlchemy + URL Dispatch Wiki Tutorial narrative + documentation and source files for each step using the updated scaffold. For + a list of updated files, see + https://github.com/Pylons/pyramid/pull/2024#issue-112676079 -- cgit v1.2.3 From 13846e641d9c6f7be65ac535c0a31fcf1f538267 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 31 Jan 2016 01:41:10 -0600 Subject: minor tweaks --- docs/tutorials/wiki2/installation.rst | 18 +++++++++--------- .../alchemy/+package+/scripts/initializedb.py | 7 +++---- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 4 ++-- .../scaffolds/alchemy/+package+/views/default.py_tmpl | 4 ++-- pyramid/scaffolds/alchemy/MANIFEST.in_tmpl | 2 +- pyramid/scaffolds/alchemy/production.ini_tmpl | 2 -- 6 files changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 1385ab8c7..960eec861 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -94,11 +94,11 @@ Install SQLite3 and its development packages If you used a package manager to install your Python or if you compiled your Python from source, then you must install SQLite3 and its development packages. If you downloaded your Python as an installer -from python.org, then you already have it installed and can proceed to -the next section :ref:`sql_making_a_project`.. +from https://www.python.org, then you already have it installed and can +proceed to the next section :ref:`sql_making_a_project`. If you need to install the SQLite3 packages, then, for example, using -the Debian system and apt-get, the command would be the following: +the Debian system and ``apt-get``, the command would be the following: .. code-block:: text @@ -133,8 +133,8 @@ the :term:`scaffold` named ``alchemy`` which generates an application that uses :term:`SQLAlchemy` and :term:`URL dispatch`. :app:`Pyramid` supplies a variety of scaffolds to generate sample -projects. We will use `pcreate`—a script that comes with Pyramid to -quickly and easily generate scaffolds, usually with a single command—to +projects. We will use `pcreate` — a script that comes with Pyramid to +quickly and easily generate scaffolds, usually with a single command — to create the scaffold for our project. By passing `alchemy` into the `pcreate` command, the script creates @@ -225,7 +225,7 @@ For a successful test run, you should see output that ends like this:: . ---------------------------------------------------------------------- Ran 1 test in 0.094s - + OK Expose test coverage information @@ -383,8 +383,8 @@ This means the server is ready to accept requests. Visit the application in a browser ================================== -In a browser, visit `http://localhost:6543/ `_. You -will see the generated application's default page. +In a browser, visit http://localhost:6543/. You will see the generated +application's default page. One thing you'll notice is the "debug toolbar" icon on right hand side of the page. You can read more about the purpose of the icon at @@ -401,7 +401,7 @@ assumptions: - you are willing to use :term:`URL dispatch` to map URLs to code -- you want to use ``ZopeTransactionExtension`` and ``pyramid_tm`` to scope +- you want to use ``zope.sqlalchemy`` and ``pyramid_tm`` to scope sessions to requests .. note:: diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index f0d09729e..4f9711d93 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models.meta import ( get_engine, get_dbmaker, ) -from ..models.mymodel import MyModel +from ..models import MyModel def usage(argv): @@ -34,12 +34,11 @@ def main(argv=sys.argv): settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) + Base.metadata.create_all(engine) + dbmaker = get_dbmaker(engine) dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) - with transaction.manager: model = MyModel(name='one', value=1) dbsession.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 074c7a773..963377b78 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -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/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl index 43fb33e05..7bf0026e5 100644 --- a/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl @@ -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': '{{project}}'} diff --git a/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl b/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl index 0ff6eb7a0..f93f45544 100644 --- a/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl +++ b/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl index 022bc0b7b..4d9f835d4 100644 --- a/pyramid/scaffolds/alchemy/production.ini_tmpl +++ b/pyramid/scaffolds/alchemy/production.ini_tmpl @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite -- cgit v1.2.3 From ab495cf5e745688640b0d1313ecf2de1e5ed2be8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 31 Jan 2016 01:44:48 -0600 Subject: add a 404 error view --- pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl | 8 ++++++++ pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl create mode 100644 pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl diff --git a/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl new file mode 100644 index 000000000..3a20f8487 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl @@ -0,0 +1,7 @@ +from pyramid.view import ( + notfound_view_config, +) + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 67f733e161f19bb2b7322edd120b9bf489154536 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 31 Jan 2016 01:46:34 -0600 Subject: stop using the meta package from the outside --- pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py | 2 +- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index 4f9711d93..0b2a42c59 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -9,7 +9,7 @@ from pyramid.paster import ( from pyramid.scripts.common import parse_vars -from ..models.meta import ( +from ..models import ( Base, get_session, get_engine, diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 963377b78..01f2cb4cc 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -13,10 +13,10 @@ 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 ( + from .models import ( get_session, get_engine, get_dbmaker, @@ -28,7 +28,7 @@ class BaseTest(unittest.TestCase): self.session = get_session(transaction.manager, dbmaker) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): -- cgit v1.2.3 From a6d08e1ad8c8bbcc7e14eabf06e26507e636d538 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 3 Feb 2016 23:46:40 -0600 Subject: improve the models api/usage and add a lot of comments --- pyramid/scaffolds/alchemy/+package+/__init__.py | 2 +- .../scaffolds/alchemy/+package+/models/__init__.py | 71 +++++++++++++++++++++- pyramid/scaffolds/alchemy/+package+/models/meta.py | 33 ---------- .../scaffolds/alchemy/+package+/models/mymodel.py | 3 +- .../alchemy/+package+/scripts/initializedb.py | 9 +-- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 8 +-- 6 files changed, 80 insertions(+), 46 deletions(-) diff --git a/pyramid/scaffolds/alchemy/+package+/__init__.py b/pyramid/scaffolds/alchemy/+package+/__init__.py index 7994bbfa8..17763812a 100644 --- a/pyramid/scaffolds/alchemy/+package+/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/__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/pyramid/scaffolds/alchemy/+package+/models/__init__.py b/pyramid/scaffolds/alchemy/+package+/models/__init__.py index 6ffc10a78..e55689f2c 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/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_sessionmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker + + +def get_tm_session(dbmaker, 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) + dbmaker = get_sessionmaker(engine) + with transaction.manager: + dbsession = get_tm_session(dbmaker, transaction.manager) + + """ + dbsession = dbmaker() + 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('{{package}}.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + dbmaker = get_sessionmaker(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(dbmaker, r.tm), + 'dbsession', + reify=True + ) diff --git a/pyramid/scaffolds/alchemy/+package+/models/meta.py b/pyramid/scaffolds/alchemy/+package+/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/meta.py +++ b/pyramid/scaffolds/alchemy/+package+/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/pyramid/scaffolds/alchemy/+package+/models/mymodel.py b/pyramid/scaffolds/alchemy/+package+/models/mymodel.py index 5a2b5890c..d65a01a42 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/mymodel.py +++ b/pyramid/scaffolds/alchemy/+package+/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/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index 0b2a42c59..13d4e543e 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -11,9 +11,9 @@ from pyramid.scripts.common import parse_vars from ..models import ( Base, - get_session, get_engine, - get_dbmaker, + get_sessionmaker, + get_tm_session, ) from ..models import MyModel @@ -36,9 +36,10 @@ def main(argv=sys.argv): engine = get_engine(settings) Base.metadata.create_all(engine) - dbmaker = get_dbmaker(engine) - dbsession = get_session(transaction.manager, dbmaker) + dbmaker = get_sessionmaker(engine) with transaction.manager: + dbsession = get_tm_session(dbmaker, transaction.manager) + model = MyModel(name='one', value=1) dbsession.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 01f2cb4cc..4eecaf33c 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -17,15 +17,15 @@ class BaseTest(unittest.TestCase): settings = self.config.get_settings() from .models import ( - get_session, get_engine, - get_dbmaker, + get_sessionmaker, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + dbmaker = get_sessionmaker(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(dbmaker, transaction.manager) def init_database(self): from .models import Base -- cgit v1.2.3 From f4e1ca8bb88f7d899f2b8c39049089a9c0e723f2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 4 Feb 2016 21:15:49 -0600 Subject: rename dbmaker to session_factory --- .../scaffolds/alchemy/+package+/models/__init__.py | 20 ++++++++++---------- .../alchemy/+package+/scripts/initializedb.py | 6 +++--- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py b/pyramid/scaffolds/alchemy/+package+/models/__init__.py index e55689f2c..c1ceacbf4 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py @@ -16,13 +16,13 @@ def get_engine(settings, prefix='sqlalchemy.'): return engine_from_config(settings, prefix) -def get_sessionmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory -def get_tm_session(dbmaker, transaction_manager): +def get_tm_session(session_factory, transaction_manager): """ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. @@ -38,12 +38,12 @@ def get_tm_session(dbmaker, transaction_manager): import transaction engine = get_engine(settings) - dbmaker = get_sessionmaker(engine) + session_factory = get_session_factory(engine) with transaction.manager: - dbsession = get_tm_session(dbmaker, transaction.manager) + dbsession = get_tm_session(session_factory, transaction.manager) """ - dbsession = dbmaker() + dbsession = session_factory() zope.sqlalchemy.register( dbsession, transaction_manager=transaction_manager) return dbsession @@ -61,12 +61,12 @@ def includeme(config): # use pyramid_tm to hook the transaction lifecycle to the request config.include('pyramid_tm') - dbmaker = get_sessionmaker(get_engine(settings)) + 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(dbmaker, r.tm), + lambda r: get_tm_session(session_factory, r.tm), 'dbsession', reify=True ) diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index 13d4e543e..da63c180a 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -12,7 +12,7 @@ from pyramid.scripts.common import parse_vars from ..models import ( Base, get_engine, - get_sessionmaker, + get_session_factory, get_tm_session, ) from ..models import MyModel @@ -36,10 +36,10 @@ def main(argv=sys.argv): engine = get_engine(settings) Base.metadata.create_all(engine) - dbmaker = get_sessionmaker(engine) + session_factory = get_session_factory(engine) with transaction.manager: - dbsession = get_tm_session(dbmaker, transaction.manager) + dbsession = get_tm_session(session_factory, transaction.manager) model = MyModel(name='one', value=1) dbsession.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 4eecaf33c..be42cb5d9 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -18,14 +18,14 @@ class BaseTest(unittest.TestCase): from .models import ( get_engine, - get_sessionmaker, + get_session_factory, get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_sessionmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_tm_session(dbmaker, transaction.manager) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): from .models import Base -- cgit v1.2.3 From 0e21c2abcf1174e73b139e7e78f599643d7f84e4 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 4 Feb 2016 21:54:09 -0600 Subject: make models/__init__.py a template --- .../scaffolds/alchemy/+package+/models/__init__.py | 72 ---------------------- .../alchemy/+package+/models/__init__.py_tmpl | 72 ++++++++++++++++++++++ .../alchemy/+package+/views/errors.py_tmpl | 4 +- 3 files changed, 73 insertions(+), 75 deletions(-) delete mode 100644 pyramid/scaffolds/alchemy/+package+/models/__init__.py create mode 100644 pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py b/pyramid/scaffolds/alchemy/+package+/models/__init__.py deleted file mode 100644 index c1ceacbf4..000000000 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py +++ /dev/null @@ -1,72 +0,0 @@ -from sqlalchemy import engine_from_config -from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm import configure_mappers -import zope.sqlalchemy - -# import or define all models here to ensure they are attached to the -# Base.metadata prior to any initialization routines -from .mymodel import MyModel # flake8: noqa - -# run configure_mappers after defining all of the models to ensure -# all relationships can be setup -configure_mappers() - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_session_factory(engine): - factory = sessionmaker() - factory.configure(bind=engine) - return factory - - -def get_tm_session(session_factory, transaction_manager): - """ - Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. - - This function will hook the session to the transaction manager which - will take care of committing any changes. - - - When using pyramid_tm it will automatically be committed or aborted - depending on whether an exception is raised. - - - When using scripts you should wrap the session in a manager yourself. - For example:: - - import transaction - - engine = get_engine(settings) - session_factory = get_session_factory(engine) - with transaction.manager: - dbsession = get_tm_session(session_factory, transaction.manager) - - """ - dbsession = session_factory() - zope.sqlalchemy.register( - dbsession, transaction_manager=transaction_manager) - return dbsession - - -def includeme(config): - """ - Initialize the model for a Pyramid app. - - Activate this setup using ``config.include('{{package}}.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/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl new file mode 100644 index 000000000..7d0c94a14 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl @@ -0,0 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines +from .mymodel import MyModel # flake8: noqa + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('{{project}}.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl index 3a20f8487..a4b8201f1 100644 --- a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl @@ -1,6 +1,4 @@ -from pyramid.view import ( - notfound_view_config, -) +from pyramid.view import notfound_view_config @notfound_view_config(renderer='../templates/404.jinja2') def notfound_view(request): -- cgit v1.2.3 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 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 +- pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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, diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index da63c180a..7307ecc5c 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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 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 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 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(-) 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 + pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl | 1 + 4 files changed, 4 insertions(+) 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( diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl index 7d0c94a14..26b50aaf6 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl @@ -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 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(-) 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(-) 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(-) 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(-) 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 ++ .../alchemy/+package+/views/errors.py_tmpl | 5 -- .../alchemy/+package+/views/notfound.py_tmpl | 7 ++ 26 files changed, 191 insertions(+), 431 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 delete mode 100644 pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl create mode 100644 pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl 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 {} diff --git a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl deleted file mode 100644 index a4b8201f1..000000000 --- a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl +++ /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/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} -- 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(-) 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(-) 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 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 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 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(-) 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(-) 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(-) 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 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(-) 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(-) 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(-) 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(-) 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(-) 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 ++ pyramid/scaffolds/alchemy/+package+/__init__.py | 3 +- pyramid/scaffolds/alchemy/+package+/routes.py | 3 + 10 files changed, 66 insertions(+), 45 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 create mode 100644 pyramid/scaffolds/alchemy/+package+/routes.py 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') diff --git a/pyramid/scaffolds/alchemy/+package+/__init__.py b/pyramid/scaffolds/alchemy/+package+/__init__.py index 17763812a..4dab44823 100644 --- a/pyramid/scaffolds/alchemy/+package+/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/__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/pyramid/scaffolds/alchemy/+package+/routes.py b/pyramid/scaffolds/alchemy/+package+/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') -- 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 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 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(-) 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(-) 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 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(-) 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 +++++ pyramid/scaffolds/alchemy/setup.py_tmpl | 5 +++++ 6 files changed, 30 insertions(+) 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] diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index af193a73d..d837ea0a9 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -19,6 +19,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='{{project}}', version='0.0', description='{{project}}', @@ -37,6 +41,7 @@ setup(name='{{project}}', include_package_data=True, zip_safe=False, test_suite='{{package}}', + 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(+) 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 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 From 6a2bdac1ec3b1fb6ed37bc8b92b807df6c0fa07d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 21 Feb 2016 03:21:10 -0800 Subject: wiki2 docs update WIP - minor grammar, .rst syntax - add pip to glossary - add pip instructions, commented until 1.7 is released --- docs/glossary.rst | 4 ++++ docs/tutorials/wiki2/background.rst | 4 ++-- docs/tutorials/wiki2/design.rst | 2 +- docs/tutorials/wiki2/index.rst | 12 ++++++------ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 2683ff369..cbf58c226 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1093,3 +1093,7 @@ Glossary A technique used when serving a cacheable static asset in order to force a client to query the new version of the asset. See :ref:`cache_busting` for more information. + + pip + The `Python Packaging Authority `_ recommended tool + for installing Python packages. diff --git a/docs/tutorials/wiki2/background.rst b/docs/tutorials/wiki2/background.rst index b8afb8305..2dac847d8 100644 --- a/docs/tutorials/wiki2/background.rst +++ b/docs/tutorials/wiki2/background.rst @@ -5,13 +5,13 @@ Background This version of the :app:`Pyramid` wiki tutorial presents a :app:`Pyramid` application that uses technologies which will be familiar to someone with SQL database experience. It uses -:term:`SQLAlchemy` as a persistence mechanism and :term:`url dispatch` to map +:term:`SQLAlchemy` as a persistence mechanism and :term:`URL dispatch` to map URLs to code. It can also be followed by people without any prior Python web framework experience. To code along with this tutorial, the developer will need a UNIX machine with development tools (Mac OS X with XCode, any Linux or BSD -variant, etc) *or* a Windows system of any kind. +variant, etc.) *or* a Windows system of any kind. .. note:: diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 45e2fddd0..929bc7806 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -10,7 +10,7 @@ Overall We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` -Python module. We will add this module in the dependency list on the project +Python module. We will add this module to the dependency list in the project's ``setup.py`` file. Models diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 74fb5bfd5..18e9f552e 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -1,15 +1,15 @@ .. _bfg_sql_wiki_tutorial: -SQLAlchemy + URL Dispatch Wiki Tutorial +SQLAlchemy + URL dispatch wiki tutorial ======================================= -This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based +This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based :app:`Pyramid` application to a developer familiar with Python. When the -tutorial is finished, the developer will have created a basic Wiki -application with authentication. +tutorial is finished, the developer will have created a basic wiki +application with authentication and authorization. -For cut and paste purposes, the source code for all stages of this -tutorial can be browsed on GitHub at `docs/tutorials/wiki2/src +For cut and paste purposes, the source code for all stages of this tutorial can +be browsed on GitHub at `docs/tutorials/wiki2/src `_, which corresponds to the same location if you have Pyramid sources. -- cgit v1.2.3 From 25fed631357e23aaa4d54c2923e8c463cd41930e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 21 Feb 2016 03:22:13 -0800 Subject: oopsie, include installation in commit --- docs/tutorials/wiki2/installation.rst | 127 ++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 53 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 5d6d8e56b..1dd71cb76 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -9,9 +9,9 @@ This tutorial assumes that you have already followed the steps in :ref:`installing_chapter`, except **do not create a virtualenv or install Pyramid**. Thereby you will satisfy the following requirements. -* Python interpreter is installed on your operating system -* :term:`setuptools` or :term:`distribute` is installed -* :term:`virtualenv` is installed +* A Python interpreter is installed on your operating system. +* :term:`virtualenv` is installed. +* :term:`pip` will be installed when we create a virtual environment. Create directory to contain the project --------------------------------------- @@ -21,38 +21,36 @@ We need a workspace for our project files. On UNIX ^^^^^^^ -.. code-block:: text +.. code-block:: bash $ mkdir ~/pyramidtut On Windows ^^^^^^^^^^ -.. code-block:: text +.. code-block:: ps1con c:\> mkdir pyramidtut Create and use a virtual Python environment ------------------------------------------- -Next let's create a `virtualenv` workspace for our project. We will -use the `VENV` environment variable instead of the absolute path of the -virtual environment. +Next let's create a ``virtualenv`` workspace for our project. We will use the +``VENV`` environment variable instead of the absolute path of the virtual +environment. On UNIX ^^^^^^^ -.. code-block:: text +.. code-block:: bash $ export VENV=~/pyramidtut $ virtualenv $VENV - New python executable in /home/foo/env/bin/python - Installing setuptools.............done. On Windows ^^^^^^^^^^ -.. code-block:: text +.. code-block:: ps1con c:\> set VENV=c:\pyramidtut @@ -61,15 +59,24 @@ path to the command for your Python version. Python 2.7: -.. code-block:: text +.. code-block:: ps1con c:\> c:\Python27\Scripts\virtualenv %VENV% -Python 3.3: +Python 3.5: -.. code-block:: text +.. code-block:: ps1con + + c:\> c:\Python35\Scripts\virtualenv %VENV% + + +.. Upgrade pip in the virtual environment + -------------------------------------- + +.. .. code-block:: bash + +.. $ $VENV/bin/pip install --upgrade pip - c:\> c:\Python33\Scripts\virtualenv %VENV% Install Pyramid into the virtual Python environment --------------------------------------------------- @@ -77,52 +84,58 @@ Install Pyramid into the virtual Python environment On UNIX ^^^^^^^ -.. code-block:: text +.. code-block:: bash $ $VENV/bin/easy_install pyramid +.. $ $VENV/bin/pip install pyramid + On Windows ^^^^^^^^^^ -.. code-block:: text +.. code-block:: ps1con c:\> %VENV%\Scripts\easy_install pyramid +.. c:\> %VENV%\Scripts\pip install pyramid + Install SQLite3 and its development packages -------------------------------------------- If you used a package manager to install your Python or if you compiled your Python from source, then you must install SQLite3 and its development packages. If you downloaded your Python as an installer -from https://www.python.org, then you already have it installed and can -proceed to the next section :ref:`sql_making_a_project`. +from https://www.python.org, then you already have it installed and can skip +this step. If you need to install the SQLite3 packages, then, for example, using the Debian system and ``apt-get``, the command would be the following: -.. code-block:: text +.. code-block:: bash $ sudo apt-get install libsqlite3-dev Change directory to your virtual Python environment --------------------------------------------------- -Change directory to the ``pyramidtut`` directory. +Change directory to the ``pyramidtut`` directory, which is both your workspace +and your virtual environment. On UNIX ^^^^^^^ -.. code-block:: text +.. code-block:: bash $ cd pyramidtut On Windows ^^^^^^^^^^ -.. code-block:: text +.. code-block:: ps1con c:\> cd pyramidtut + .. _sql_making_a_project: Making a project @@ -132,30 +145,29 @@ Your next step is to create a project. For this tutorial we will use the :term:`scaffold` named ``alchemy`` which generates an application that uses :term:`SQLAlchemy` and :term:`URL dispatch`. -:app:`Pyramid` supplies a variety of scaffolds to generate sample -projects. We will use `pcreate` — a script that comes with Pyramid to -quickly and easily generate scaffolds, usually with a single command — to -create the scaffold for our project. +:app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We +will use ``pcreate``, a script that comes with Pyramid, to create our project +using a scaffold. -By passing `alchemy` into the `pcreate` command, the script creates -the files needed to use SQLAlchemy. By passing in our application name -`tutorial`, the script inserts that application name into all the -required files. For example, `pcreate` creates the -``initialize_tutorial_db`` in the ``pyramidtut/bin`` directory. +By passing ``alchemy`` into the ``pcreate`` command, the script creates the +files needed to use SQLAlchemy. By passing in our application name +``tutorial``, the script inserts that application name into all the required +files. For example, ``pcreate`` creates the ``initialize_tutorial_db`` in the +``pyramidtut/bin`` directory. The below instructions assume your current working directory is "pyramidtut". On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/pcreate -s alchemy tutorial On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial @@ -165,34 +177,39 @@ On Windows startup problems, try putting both the virtualenv and the project into directories that do not contain spaces in their paths. + .. _installing_project_in_dev_mode: Installing the project in development mode ========================================== -In order to do development on the project easily, you must "register" -the project as a development egg in your workspace using the -``setup.py develop`` command. In order to do so, cd to the `tutorial` -directory you created in :ref:`sql_making_a_project`, and run the -``setup.py develop`` command using the virtualenv Python interpreter. +In order to do development on the project easily, you must "register" the +project as a development egg in your workspace using the ``setup.py develop`` +command. In order to do so, change directory to the ``tutorial`` directory that +you created in :ref:`sql_making_a_project`, and run the ``setup.py develop`` +command using the virtualenv Python interpreter. On UNIX ------- -.. code-block:: text +.. code-block:: bash $ cd tutorial $ $VENV/bin/python setup.py develop +.. $ $VENV/bin/pip install -e . + On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -The console will show `setup.py` checking for packages and installing +.. c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . + +The console will show ``setup.py`` checking for packages and installing missing packages. Success executing this command will show a line like the following:: @@ -209,17 +226,21 @@ the tests for the project. On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py test -q +.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 + On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q +.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 + For a successful test run, you should see output that ends like this:: . @@ -243,14 +264,14 @@ To get this functionality working, we'll need to install the ``nose`` and On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/easy_install nose coverage On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\easy_install nose coverage @@ -260,14 +281,14 @@ coverage tests. On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \ --cover-erase --with-coverage @@ -311,14 +332,14 @@ directory (the directory with a ``development.ini`` in it): On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/initialize_tutorial_db development.ini On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini @@ -363,14 +384,14 @@ Start the application. On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/pserve development.ini --reload On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload -- cgit v1.2.3 From 798e8549dfc8dc779b2ce5bd5552f5603fef60ea Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Sun, 21 Feb 2016 22:19:45 +0100 Subject: Fix the import from meta --- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index be42cb5d9..072eab5b2 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -28,7 +28,7 @@ class BaseTest(unittest.TestCase): self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models import Base + from .models.meta import Base Base.metadata.create_all(self.engine) def tearDown(self): -- cgit v1.2.3 From 93fa23ae1463e78230cc3af8ecb89a6813b5556b Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 21 Feb 2016 15:51:55 -0700 Subject: Remove references to pip --- docs/glossary.rst | 4 ---- docs/tutorials/wiki2/design.rst | 2 +- docs/tutorials/wiki2/index.rst | 12 ++++++------ docs/tutorials/wiki2/installation.rst | 26 +++----------------------- 4 files changed, 10 insertions(+), 34 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index cbf58c226..2683ff369 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1093,7 +1093,3 @@ Glossary A technique used when serving a cacheable static asset in order to force a client to query the new version of the asset. See :ref:`cache_busting` for more information. - - pip - The `Python Packaging Authority `_ recommended tool - for installing Python packages. diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 929bc7806..45e2fddd0 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -10,7 +10,7 @@ Overall We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` -Python module. We will add this module to the dependency list in the project's +Python module. We will add this module in the dependency list on the project ``setup.py`` file. Models diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 18e9f552e..74fb5bfd5 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -1,15 +1,15 @@ .. _bfg_sql_wiki_tutorial: -SQLAlchemy + URL dispatch wiki tutorial +SQLAlchemy + URL Dispatch Wiki Tutorial ======================================= -This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based +This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based :app:`Pyramid` application to a developer familiar with Python. When the -tutorial is finished, the developer will have created a basic wiki -application with authentication and authorization. +tutorial is finished, the developer will have created a basic Wiki +application with authentication. -For cut and paste purposes, the source code for all stages of this tutorial can -be browsed on GitHub at `docs/tutorials/wiki2/src +For cut and paste purposes, the source code for all stages of this +tutorial can be browsed on GitHub at `docs/tutorials/wiki2/src `_, which corresponds to the same location if you have Pyramid sources. diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 1dd71cb76..eb6cf50e0 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -9,9 +9,9 @@ This tutorial assumes that you have already followed the steps in :ref:`installing_chapter`, except **do not create a virtualenv or install Pyramid**. Thereby you will satisfy the following requirements. -* A Python interpreter is installed on your operating system. -* :term:`virtualenv` is installed. -* :term:`pip` will be installed when we create a virtual environment. +* Python interpreter is installed on your operating system +* :term:`setuptools` or :term:`distribute` is installed +* :term:`virtualenv` is installed Create directory to contain the project --------------------------------------- @@ -70,14 +70,6 @@ Python 3.5: c:\> c:\Python35\Scripts\virtualenv %VENV% -.. Upgrade pip in the virtual environment - -------------------------------------- - -.. .. code-block:: bash - -.. $ $VENV/bin/pip install --upgrade pip - - Install Pyramid into the virtual Python environment --------------------------------------------------- @@ -88,8 +80,6 @@ On UNIX $ $VENV/bin/easy_install pyramid -.. $ $VENV/bin/pip install pyramid - On Windows ^^^^^^^^^^ @@ -97,8 +87,6 @@ On Windows c:\> %VENV%\Scripts\easy_install pyramid -.. c:\> %VENV%\Scripts\pip install pyramid - Install SQLite3 and its development packages -------------------------------------------- @@ -197,8 +185,6 @@ On UNIX $ cd tutorial $ $VENV/bin/python setup.py develop -.. $ $VENV/bin/pip install -e . - On Windows ---------- @@ -207,8 +193,6 @@ On Windows c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -.. c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . - The console will show ``setup.py`` checking for packages and installing missing packages. Success executing this command will show a line like the following:: @@ -230,8 +214,6 @@ On UNIX $ $VENV/bin/python setup.py test -q -.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 - On Windows ---------- @@ -239,8 +221,6 @@ On Windows c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q -.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 - For a successful test run, you should see output that ends like this:: . -- cgit v1.2.3 From 1c0a3d7e326b06dc4ea0e015ab25c4dedb754342 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 21 Feb 2016 23:12:19 -0800 Subject: revert a couple of the reversions --- docs/tutorials/wiki2/design.rst | 2 +- docs/tutorials/wiki2/index.rst | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 45e2fddd0..929bc7806 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -10,7 +10,7 @@ Overall We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` -Python module. We will add this module in the dependency list on the project +Python module. We will add this module to the dependency list in the project's ``setup.py`` file. Models diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 74fb5bfd5..18e9f552e 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -1,15 +1,15 @@ .. _bfg_sql_wiki_tutorial: -SQLAlchemy + URL Dispatch Wiki Tutorial +SQLAlchemy + URL dispatch wiki tutorial ======================================= -This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based +This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based :app:`Pyramid` application to a developer familiar with Python. When the -tutorial is finished, the developer will have created a basic Wiki -application with authentication. +tutorial is finished, the developer will have created a basic wiki +application with authentication and authorization. -For cut and paste purposes, the source code for all stages of this -tutorial can be browsed on GitHub at `docs/tutorials/wiki2/src +For cut and paste purposes, the source code for all stages of this tutorial can +be browsed on GitHub at `docs/tutorials/wiki2/src `_, which corresponds to the same location if you have Pyramid sources. -- cgit v1.2.3 From 8571f2bf08afed56f75d793f4f1676d1d86a9dac Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 22 Feb 2016 00:08:34 -0800 Subject: update installation - update command line output to reflect changes to scaffold - fix inconsistent heading levels - add spacing to separate sections - minor grammar and syntax --- docs/tutorials/wiki2/installation.rst | 222 +++++++++++++++++----------------- 1 file changed, 113 insertions(+), 109 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index eb6cf50e0..891305bf5 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -3,7 +3,7 @@ Installation ============ Before you begin -================ +---------------- This tutorial assumes that you have already followed the steps in :ref:`installing_chapter`, except **do not create a virtualenv or install @@ -13,6 +13,7 @@ Pyramid**. Thereby you will satisfy the following requirements. * :term:`setuptools` or :term:`distribute` is installed * :term:`virtualenv` is installed + Create directory to contain the project --------------------------------------- @@ -32,6 +33,7 @@ On Windows c:\> mkdir pyramidtut + Create and use a virtual Python environment ------------------------------------------- @@ -54,7 +56,7 @@ On Windows c:\> set VENV=c:\pyramidtut -Versions of Python use different paths, so you will need to adjust the +Each version of Python uses different paths, so you will need to adjust the path to the command for your Python version. Python 2.7: @@ -87,22 +89,23 @@ On Windows c:\> %VENV%\Scripts\easy_install pyramid + Install SQLite3 and its development packages -------------------------------------------- -If you used a package manager to install your Python or if you compiled -your Python from source, then you must install SQLite3 and its -development packages. If you downloaded your Python as an installer -from https://www.python.org, then you already have it installed and can skip -this step. +If you used a package manager to install your Python or if you compiled your +Python from source, then you must install SQLite3 and its development packages. +If you downloaded your Python as an installer from https://www.python.org, then +you already have it installed and can skip this step. -If you need to install the SQLite3 packages, then, for example, using -the Debian system and ``apt-get``, the command would be the following: +If you need to install the SQLite3 packages, then, for example, using the +Debian system and ``apt-get``, the command would be the following: .. code-block:: bash $ sudo apt-get install libsqlite3-dev + Change directory to your virtual Python environment --------------------------------------------------- @@ -127,7 +130,7 @@ On Windows .. _sql_making_a_project: Making a project -================ +---------------- Your next step is to create a project. For this tutorial we will use the :term:`scaffold` named ``alchemy`` which generates an application @@ -146,30 +149,29 @@ files. For example, ``pcreate`` creates the ``initialize_tutorial_db`` in the The below instructions assume your current working directory is "pyramidtut". On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/pcreate -s alchemy tutorial On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial -.. note:: If you are using Windows, the ``alchemy`` - scaffold may not deal gracefully with installation into a - location that contains spaces in the path. If you experience - startup problems, try putting both the virtualenv and the project - into directories that do not contain spaces in their paths. +.. note:: If you are using Windows, the ``alchemy`` scaffold may not deal + gracefully with installation into a location that contains spaces in the + path. If you experience startup problems, try putting both the virtualenv + and the project into directories that do not contain spaces in their paths. .. _installing_project_in_dev_mode: Installing the project in development mode -========================================== +------------------------------------------ In order to do development on the project easily, you must "register" the project as a development egg in your workspace using the ``setup.py develop`` @@ -178,7 +180,7 @@ you created in :ref:`sql_making_a_project`, and run the ``setup.py develop`` command using the virtualenv Python interpreter. On UNIX -------- +^^^^^^^ .. code-block:: bash @@ -186,36 +188,35 @@ On UNIX $ $VENV/bin/python setup.py develop On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -The console will show ``setup.py`` checking for packages and installing -missing packages. Success executing this command will show a line like -the following:: +The console will show ``setup.py`` checking for packages and installing missing +packages. Success executing this command will show a line like the following:: Finished processing dependencies for tutorial==0.0 .. _sql_running_tests: Run the tests -============= +------------- -After you've installed the project in development mode, you may run -the tests for the project. +After you've installed the project in development mode, you may run the tests +for the project. On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/python setup.py test -q On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con @@ -223,50 +224,49 @@ On Windows For a successful test run, you should see output that ends like this:: - . - ---------------------------------------------------------------------- - Ran 1 test in 0.094s + .. + ---------------------------------------------------------------------- + Ran 2 tests in 0.053s - OK + OK Expose test coverage information -================================ +-------------------------------- -You can run the ``nosetests`` command to see test coverage -information. This runs the tests in the same way that ``setup.py -test`` does but provides additional "coverage" information, exposing -which lines of your project are "covered" (or not covered) by the -tests. +You can run the ``nosetests`` command to see test coverage information. This +runs the tests in the same way that ``setup.py test`` does, but provides +additional "coverage" information, exposing which lines of your project are +covered by the tests. To get this functionality working, we'll need to install the ``nose`` and ``coverage`` packages into our ``virtualenv``: On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/easy_install nose coverage On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\easy_install nose coverage -Once ``nose`` and ``coverage`` are installed, we can actually run the -coverage tests. +Once ``nose`` and ``coverage`` are installed, we can run the tests with +coverage. On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con @@ -275,34 +275,38 @@ On Windows If successful, you will see output something like this:: - . - Name Stmts Miss Cover Missing - --------------------------------------------------- - tutorial.py 13 9 31% 13-21 - tutorial/models.py 12 0 100% - tutorial/scripts.py 0 0 100% - tutorial/views.py 11 0 100% - --------------------------------------------------- - TOTAL 36 9 75% - ---------------------------------------------------------------------- - Ran 2 tests in 0.643s + .. + Name Stmts Miss Cover Missing + ---------------------------------------------------------- + tutorial.py 8 6 25% 7-12 + tutorial/models.py 22 0 100% + tutorial/models/meta.py 5 0 100% + tutorial/models/mymodel.py 8 0 100% + tutorial/scripts.py 0 0 100% + tutorial/views.py 0 0 100% + tutorial/views/default.py 12 0 100% + ---------------------------------------------------------- + TOTAL 55 6 89% + ---------------------------------------------------------------------- + Ran 2 tests in 0.579s - OK + OK + +Our package doesn't quite have 100% test coverage. -Looks like our package doesn't quite have 100% test coverage. .. _initialize_db_wiki2: Initializing the database -========================= +------------------------- -We need to use the ``initialize_tutorial_db`` :term:`console -script` to initialize our 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 + The ``initialize_tutorial_db`` command does not perform a migration, but + rather it simply creates missing tables and adds some dummy data. If you already have a database, you should delete it before running ``initialize_tutorial_db`` again. @@ -310,14 +314,14 @@ Type the following command, making sure you are still in the ``tutorial`` directory (the directory with a ``development.ini`` in it): On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/initialize_tutorial_db development.ini On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con @@ -325,51 +329,51 @@ On Windows The output to your console should be something like this:: - 2015-05-23 16:49:49,609 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 - 2015-05-23 16:49:49,609 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 - 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models") - 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-23 16:49:49,612 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] - CREATE TABLE models ( - id INTEGER NOT NULL, - name TEXT, - value INTEGER, - PRIMARY KEY (id) - ) - - - 2015-05-23 16:49:49,612 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name) - 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-23 16:49:49,614 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - 2015-05-23 16:49:49,616 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) - 2015-05-23 16:49:49,617 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?) - 2015-05-23 16:49:49,617 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1) - 2015-05-23 16:49:49,618 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - -Success! You should now have a ``tutorial.sqlite`` file in your current working -directory. This will be a SQLite database with a single table defined in it + 2016-02-21 23:57:41,793 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 + 2016-02-21 23:57:41,793 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-02-21 23:57:41,794 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 + 2016-02-21 23:57:41,794 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-02-21 23:57:41,796 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models") + 2016-02-21 23:57:41,796 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE models ( + id INTEGER NOT NULL, + name TEXT, + value INTEGER, + CONSTRAINT pk_models PRIMARY KEY (id) + ) + + + 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name) + 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-21 23:57:41,801 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) + 2016-02-21 23:57:41,802 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?) + 2016-02-21 23:57:41,802 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1) + 2016-02-21 23:57:41,821 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + +Success! You should now have a ``tutorial.sqlite`` file in your current +working directory. This is an SQLite database with a single table defined in it (``models``). .. _wiki2-start-the-application: Start the application -===================== +--------------------- Start the application. On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/pserve development.ini --reload On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con @@ -382,14 +386,15 @@ On Windows If successful, you will see something like this on your console:: - Starting subprocess with file monitor - Starting server in PID 8966. - Starting HTTP server on http://0.0.0.0:6543 + Starting subprocess with file monitor + Starting server in PID 82349. + serving on http://127.0.0.1:6543 This means the server is ready to accept requests. + Visit the application in a browser -================================== +---------------------------------- In a browser, visit http://localhost:6543/. You will see the generated application's default page. @@ -399,8 +404,9 @@ page. You can read more about the purpose of the icon at :ref:`debug_toolbar`. It allows you to get information about your application while you develop. + Decisions the ``alchemy`` scaffold has made for you -=================================================== +--------------------------------------------------- Creating a project using the ``alchemy`` scaffold makes the following assumptions: @@ -409,21 +415,19 @@ assumptions: - You are willing to use :term:`URL dispatch` to map URLs to code. -- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package - 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. +- You want to use pyramid_jinja2_ to render your templates. Different + templating engines can be used, but we had to choose one to make this + tutorial. See :ref:`available_template_system_bindings` for some options. .. note:: :app:`Pyramid` supports any persistent storage mechanism (e.g., object - database or filesystem files). It also supports an additional - mechanism to map URLs to code (:term:`traversal`). However, for the - purposes of this tutorial, we'll only be using URL dispatch and - SQLAlchemy. + database or filesystem files). It also supports an additional mechanism to + map URLs to code (:term:`traversal`). However, for the purposes of this + tutorial, we'll only be using URL dispatch and SQLAlchemy. .. _pyramid_jinja2: http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ -- cgit v1.2.3 From 4f87e545ed9596b9be5ca5958e80be55dba28a87 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 22 Feb 2016 00:58:03 -0800 Subject: update basiclayout - minor grammar and syntax - insert complete mymodel.py code --- docs/tutorials/wiki2/basiclayout.rst | 126 ++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 1ae51eb93..1310e0969 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -16,15 +16,14 @@ 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/__init__.py``. It should already contain the -following: +Open ``tutorial/__init__.py``. It should already contain the following: .. literalinclude:: src/basiclayout/tutorial/__init__.py :linenos: :language: py -Let's go over this piece-by-piece. First, we need some imports to support -later code: +Let's go over this piece-by-piece. First we need some imports to support later +code: .. literalinclude:: src/basiclayout/tutorial/__init__.py :end-before: main @@ -52,10 +51,10 @@ Next in ``main``, construct a :term:`Configurator` object: :lineno-match: :language: py -``settings`` is passed to the Configurator as a keyword argument with the -dictionary values passed as the ``**settings`` argument. This will be a +``settings`` is passed to the ``Configurator`` as a keyword argument with the +dictionary values passed as the ``**settings`` argument. This will be a dictionary of settings parsed from the ``.ini`` file, which contains -deployment-related values such as ``pyramid.reload_templates``, +deployment-related values, such as ``pyramid.reload_templates``, ``sqlalchemy.url``, and so on. Next include :term:`Jinja2` templating bindings so that we can use renderers @@ -66,16 +65,16 @@ with the ``.jinja2`` extension within our project. :lineno-match: :language: py -Next include the the package ``models`` using a dotted Python path. The -exact setup of the models will be covered later. +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-match: :language: py -Next include the ``routes`` module using a dotted Python path. This module -will be explained in the next section. +Next include the ``routes`` module using a dotted Python path. This module will +be explained in the next section. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 10 @@ -84,16 +83,16 @@ will be explained in the next section. .. note:: - Pyramid's :meth:`pyramid.config.Configurator.include` method is the - primary mechanism for extending the configurator and breaking your code - into feature-focused modules. + Pyramid's :meth:`pyramid.config.Configurator.include` method is the primary + mechanism for extending the configurator and breaking your code into + feature-focused modules. ``main`` next calls the ``scan`` method of the configurator (:meth:`pyramid.config.Configurator.scan`), which will recursively scan our -``tutorial`` package, looking for ``@view_config`` (and -other special) decorators. When it finds a ``@view_config`` decorator, a -view configuration will be registered, which will allow one of our -application URLs to be mapped to some code. +``tutorial`` package, looking for ``@view_config`` and other special +decorators. When it finds a ``@view_config`` decorator, a view configuration +will be registered, allowing one of our application URLs to be mapped to some +code. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 11 @@ -113,31 +112,31 @@ Finally ``main`` is finished configuring things, so it uses the Route declarations ------------------ -Open the ``tutorials/routes.py`` file. It should already contain the -following: +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). +On line 2, we call :meth:`pyramid.config.Configurator.add_static_view` with +three arguments: ``static`` (the name), ``static`` (the path), and +``cache_max_age`` (a keyword argument). This registers a static resource view which will match any URL that starts with the prefix ``/static`` (by virtue of the first argument to -``add_static_view``). This will serve up static resources for us from within -the ``static`` directory of our ``tutorial`` package, in this case, via +``add_static_view``). This will serve up static resources for us from within +the ``static`` directory of our ``tutorial`` package, in this case via ``http://localhost:6543/static/`` and below (by virtue of the second argument to ``add_static_view``). With this declaration, we're saying that any URL that starts with ``/static`` should go to the static view; any remainder of its -path (e.g. the ``/foo`` in ``/static/foo``) will be used to compose a path to +path (e.g., the ``/foo`` in ``/static/foo``) will be used to compose a path to a static file resource, such as a CSS file. -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/``. +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 @@ -148,15 +147,15 @@ 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/views/default.py`` in the ``views`` package. It -should already contain the following: +Open ``tutorial/views/default.py`` in the ``views`` package. It should already +contain the following: .. literalinclude:: src/basiclayout/tutorial/views/default.py :linenos: :language: py The important part here is that the ``@view_config`` decorator associates the -function it decorates (``my_view``) with a :term:`view configuration`, +function it decorates (``my_view``) with a :term:`view configuration`, consisting of: * a ``route_name`` (``home``) @@ -172,12 +171,11 @@ Note that ``my_view()`` accepts a single argument named ``request``. This is the standard call signature for a Pyramid :term:`view callable`. Remember in our ``__init__.py`` when we executed the -:meth:`pyramid.config.Configurator.scan` method ``config.scan()``? The -purpose of calling the scan method was to find and process this -``@view_config`` decorator in order to create a view configuration within our -application. Without being processed by ``scan``, the decorator effectively -does nothing. ``@view_config`` is inert without being detected via a -:term:`scan`. +:meth:`pyramid.config.Configurator.scan` method ``config.scan()``? The purpose +of calling the scan method was to find and process this ``@view_config`` +decorator in order to create a view configuration within our application. +Without being processed by ``scan``, the decorator effectively does nothing. +``@view_config`` is inert without being detected via a :term:`scan`. The sample ``my_view()`` created by the scaffold uses a ``try:`` and ``except:`` clause to detect if there is a problem accessing the project @@ -189,12 +187,12 @@ to inform the user about possible actions to take to solve the problem. Content models with the ``models`` package ------------------------------------------ -In a SQLAlchemy-based application, a *model* object is an object composed by +In an SQLAlchemy-based application, a *model* object is an object composed by querying the SQL database. The ``models`` package is where the ``alchemy`` scaffold put the classes that implement our models. -First, open ``tutorial/models/meta.py``, which should already contain -the following: +First, open ``tutorial/models/meta.py``, which should already contain the +following: .. literalinclude:: src/basiclayout/tutorial/models/meta.py :linenos: @@ -213,10 +211,10 @@ Next we create a ``metadata`` object from the class :class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value 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 +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 @@ -225,17 +223,25 @@ application's database schema. :linenos: :language: py -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``: +Next open ``tutorial/models/mymodel.py``, which should already contain the +following: + +.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py + :linenos: + :language: py + +Notice we've defined the ``models`` as a package to make it straightforward for +defining models in separate modules. To give a simple example of a model class, +we have defined one named ``MyModel`` in ``mymodel.py``: .. literalinclude:: src/basiclayout/tutorial/models/mymodel.py :pyobject: MyModel + :lineno-match: :linenos: :language: py Our example model does not require an ``__init__`` method because SQLAlchemy -supplies for us a default constructor if one is not already present, which +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: @@ -259,14 +265,14 @@ Our ``models/__init__.py`` module defines the primary API we will use for configuring the database connections within our application, and it contains several functions we will cover below. -As we mentioned above, the purpose of the ``models.meta.metadata`` object is -to describe the schema of the database. This is done by defining models that -inherit from the ``Base`` 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 +As we mentioned above, the purpose of the ``models.meta.metadata`` object is to +describe the schema of the database. This is done by defining models that +inherit from the ``Base`` object attached to that ``metadata`` object. In +Python, code is only executed if it is imported, and so to attach the +``models`` table defined in ``mymodel.py`` to the ``metadata``, we must import +it. If we skip this step, then later, when we run :meth:`sqlalchemy.schema.MetaData.create_all`, the table will not be created -because the ``metadata`` does not know about it! +because the ``metadata`` object does not know about it! Another important reason to import all of the models is that, when defining relationships between models, they must all exist in order for SQLAlchemy to @@ -313,9 +319,9 @@ Finally, we define an ``includeme`` function, which is a hook for use with :meth:`pyramid.config.Configurator.include` to activate code in a Pyramid application add-on. It is the code that is executed above when we ran ``config.include('.models')`` in our application's ``main`` function. This -function will take the settings from the application, create an engine, -and define a ``request.dbsession`` property, which we can use to do work -on behalf of an incoming request to our application. +function will take the settings from the application, create an engine, and +define a ``request.dbsession`` property, which we can use to do work on behalf +of an incoming request to our application. .. literalinclude:: src/basiclayout/tutorial/models/__init__.py :pyobject: includeme -- cgit v1.2.3 From 15fb09b473e55f1edf60466f9fa499a5c8fbb93b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 23 Feb 2016 02:10:50 -0800 Subject: update definingmodels (WIP) - minor grammar and syntax - define hashing and its purpose --- docs/tutorials/wiki2/definingmodels.rst | 54 ++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 41f36fa26..b0781dfe4 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -20,10 +20,10 @@ 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. +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`` +We need to add a dependency, the ``bcrypt`` package, to our ``tutorial`` package's ``setup.py`` file by assigning this dependency to the ``requires`` parameter in the ``setup()`` function. @@ -56,7 +56,7 @@ On UNIX: On Windows: -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop @@ -70,8 +70,8 @@ like this:: 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. +Let's delete the file ``tutorial/models/mymodel.py``. The ``MyModel`` class is +only a sample and we're not going to use it. Add ``user.py`` @@ -85,26 +85,32 @@ Create a new file ``tutorial/models/user.py`` with the following contents: 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. +We discussed briefly in the previous chapter that our models will inherit from +an SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This will +attach the model to our schema. As you can see, our ``User`` class has a class-level attribute -``__tablename__`` which equals the string ``users``. Our ``User`` class -will also have class-level attributes named ``id``, ``name``, -``password_hash`` and ``role`` (all instances of -:class:`sqlalchemy.schema.Column`). These will map to columns in the ``users`` -table. The ``id`` attribute will be the primary key in the table. The ``name`` -attribute will be a text column, each value of which needs to be unique within -the column. The ``password_hash`` is a nullable text attribute that will -contain a securely hashed password [1]_. Finally, the ``role`` text attribute -will hold the role of the user. - -There are two helper methods that will help us later when using the -user objects. The first is ``set_password`` which will take a raw password -and transform it using bcrypt_ into an irreversible representation. The -``check_password`` method will allow us to compare input passwords to -see if they resolve to the same hash signifying a match. +``__tablename__`` which equals the string ``users``. Our ``User`` class will +also have class-level attributes named ``id``, ``name``, ``password_hash``, +and ``role`` (all instances of :class:`sqlalchemy.schema.Column`). These will +map to columns in the ``users`` table. The ``id`` attribute will be the primary +key in the table. The ``name`` attribute will be a text column, each value of +which needs to be unique within the column. The ``password_hash`` is a nullable +text attribute that will contain a securely hashed password [1]_. Finally, the +``role`` text attribute will hold the role of the user. + +There are two helper methods that will help us later when using the user +objects. The first is ``set_password`` which will take a raw password and +transform it using bcrypt_ into an irreversible representation, a process known +as "hashing". The second method, ``check_password``, will allow us to compare +the hashed value of the submitted password against the hashed value of the +password stored in the user's record in the database. If the two hashed values +match, then the submitted password is valid, and we can authenticate the user. + +We hash passwords so that it is impossible to decrypt them and use them to +authenticate in the application. If we stored passwords foolishly in clear +text, then anyone with access to the database could retrieve any password to +authenticate as any user. Add ``page.py`` -- cgit v1.2.3 From 2dc061b569fcfc8d370efb358576a71d233f514f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 24 Feb 2016 01:10:22 -0800 Subject: update definingmodels (done) - minor grammar and syntax --- docs/tutorials/wiki2/definingmodels.rst | 45 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index b0781dfe4..14099582c 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -123,16 +123,16 @@ Create a new file ``tutorial/models/page.py`` with the following contents: :language: py As you can see, our ``Page`` class is very similar to the ``User`` defined -above except with attributes focused on storing information about a wiki -page including ``id``, ``name``, and ``data``. The only new construct -introduced here is the ``creator_id`` column which is a foreign key -referencing the ``users`` table. Foreign keys are very useful at the -schema-level but since we want to relate ``User`` objects with ``Page`` -objects we also define a 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. +above, except with attributes focused on storing information about a wiki page, +including ``id``, ``name``, and ``data``. The only new construct introduced +here is the ``creator_id`` column, which is a foreign key referencing the +``users`` table. Foreign keys are very useful at the schema-level, but since we +want to relate ``User`` objects with ``Page`` objects, we also define a +``creator`` attribute as an ORM-level mapping between the two tables. +SQLAlchemy will automatically populate this value using the foreign key +referencing the user. Since the foreign key has ``nullable=False``, we are +guaranteed that an instance of ``page`` will have a corresponding +``page.creator``, which will be a ``User`` instance. Edit ``models/__init__.py`` @@ -149,8 +149,7 @@ the following: :language: py :emphasize-lines: 8,9 -Here we need to align our imports with the names of the models ``User``, -and ``Page``. +Here we align our imports with the names of the models, ``User`` and ``Page``. Edit ``scripts/initializedb.py`` @@ -163,12 +162,12 @@ 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 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``. +``MyModel`` with those of ``User`` and ``Page``. We'll also change the very end +of the script to create two ``User`` objects (``basic`` and ``editor``) as well +as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``. -Open ``tutorial/scripts/initializedb.py`` and edit it to look like -the following: +Open ``tutorial/scripts/initializedb.py`` and edit it to look like the +following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: @@ -181,9 +180,9 @@ Only the highlighted lines need to be changed. Installing the project and re-initializing the database ------------------------------------------------------- -Because our model has changed, in order to reinitialize the database, we need -to rerun the ``initialize_tutorial_db`` command to pick up the changes you've -made to both the models.py file and to the initializedb.py file. See +Because our model has changed, and in order to reinitialize the database, we +need to rerun the ``initialize_tutorial_db`` command to pick up the changes +we've made to both the models.py file and to the initializedb.py file. See :ref:`initialize_db_wiki2` for instructions. Success will look something like this:: @@ -237,9 +236,9 @@ View the application in a browser We can't. At this point, our system is in a "non-runnable" state; we'll need to change view-related files in the next chapter to be able to start the -application successfully. If you try to start the application (See -:ref:`wiki2-start-the-application`), you'll wind -up with a Python traceback on your console that ends with this exception: +application successfully. If you try to start the application (see +:ref:`wiki2-start-the-application`), you'll wind up with a Python traceback on +your console that ends with this exception: .. code-block:: text -- cgit v1.2.3 From c956c9bf07607dbd66f092a11b8cbd1d359829df Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 26 Feb 2016 02:02:59 -0800 Subject: update definingviews (done) - minor grammar and syntax --- docs/tutorials/wiki2/definingviews.rst | 249 +++++++++++++++++---------------- 1 file changed, 130 insertions(+), 119 deletions(-) diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 184f9e1fa..e48980dc8 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -3,10 +3,10 @@ Defining Views ============== A :term:`view callable` in a :app:`Pyramid` application is typically a simple -Python function that accepts a single parameter named :term:`request`. A -view callable is assumed to return a :term:`response` object. +Python function that accepts a single parameter named :term:`request`. A view +callable is assumed to return a :term:`response` object. -The request object has a dictionary as an attribute named ``matchdict``. A +The request object has a dictionary as an attribute named ``matchdict``. A ``matchdict`` maps the placeholders in the matching URL ``pattern`` to the substrings of the path in the :term:`request` URL. For instance, if a call to :meth:`pyramid.config.Configurator.add_route` has the pattern ``/{one}/{two}``, @@ -14,10 +14,11 @@ 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'}``. + Adding the ``docutils`` dependency ================================== -Remember in the previous chapter we added a new dependency on the ``bcrypt`` +Remember in the previous chapter we added a new dependency of the ``bcrypt`` package. Again, the view code in our application will depend on a package which is not a dependency of the original "tutorial" application. @@ -35,7 +36,8 @@ Open ``tutorial/setup.py`` and edit it to look like the following: 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. +installed, so re-run the ``python setup.py develop`` command. + Static assets ------------- @@ -46,52 +48,54 @@ were provided at the time we created the project. As an example, the CSS file will be accessed via ``http://localhost:6543/static/theme.css`` by virtue of the call to the -``add_static_view`` directive we've made in the ``routes.py`` file. Any -number and type of static assets can be placed in this directory (or -subdirectories) and are just referred to by URL or by using the convenience -method ``static_url``, e.g., -``request.static_url(':static/foo.css')`` within templates. +``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 ``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. +This is the `URL Dispatch` tutorial, so let's start by adding some URL patterns +to our app. Later we'll attach views to handle the URLs. -The ``routes.py`` file contains -:meth:`pyramid.config.Configurator.add_route` 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. +The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route` +calls which serve to add routes to our application. First we'll get rid of the +existing route created by the template using the name ``'home'``. It's only an +example and isn't relevant to our application. -We then need to add four calls to ``add_route``. Note that the *ordering* of -these declarations is very important. ``route`` declarations are matched in -the order they're registered. +We then need to add four calls to ``add_route``. Note that the *ordering* of +these declarations is very important. Route declarations are matched in the +order they're registered. -#. 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 ``/`` (signifying the root URL) to + the route named ``view_wiki``. In the next step, we will map it to our + ``view_wiki`` view callable by virtue of the ``@view_config`` decorator + attached to the ``view_wiki`` view function, which in turn will be indicated + by ``route_name='view_wiki'``. #. 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'``. + ``view_page``. This is the regular view for a page. Again, in the next step, + we will map it to our ``view_page`` view callable by virtue of the + ``@view_config`` decorator attached to the ``view_page`` view function, + whin in turn will be indicated by ``route_name='view_page'``. #. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the - route named ``add_page``. This is the add view for a new page. 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'``. + route named ``add_page``. This is the add view for a new page. We will map + it to our ``add_page`` view callable by virtue of the ``@view_config`` + decorator attached to the ``add_page`` view function, which in turn will be + indicated by ``route_name='add_page'``. #. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the - route named ``edit_page``. This is the edit view for a page. It maps + route named ``edit_page``. This is the edit view for a page. We will map it to our ``edit_page`` view callable by virtue of the ``@view_config`` - attached to the ``edit_page`` view function indicating - ``route_name='edit_page'``. + decorator attached to the ``edit_page`` view function, which in turn will be + indicated by ``route_name='edit_page'``. -As a result of our edits, the ``routes.py`` file should look something like: +As a result of our edits, the ``routes.py`` file should look like the +following: .. literalinclude:: src/views/tutorial/routes.py :linenos: @@ -103,13 +107,14 @@ The highlighted lines are the ones that need to be added or edited. .. warning:: The order of the routes is important! If you placed - ``/{pagename}/edit_page`` **before** ``/add_page/{pagename}`` then we would - never be able to add pages 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 + ``/{pagename}/edit_page`` *before* ``/add_page/{pagename}``, then we would + never be able to add pages. This is because the first route would always + match a request to ``/add_page/edit_page`` whereas we want ``/add_page/..`` + to have priority. This isn't a huge problem in this particular app because + wiki pages are always camel case, but it's important to be aware of this behavior in your own apps. + Adding view functions in ``views/default.py`` ============================================= @@ -131,23 +136,24 @@ and isn't relevant to our application. We also deleted the ``db_err_msg`` string. Then we added four :term:`view callable` functions to our ``views/default.py`` -module: +module, as mentioned in the previous step: * ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. * ``view_page()`` - Displays an individual page. -* ``add_page()`` - Allows the user to add a page. * ``edit_page()`` - Allows the user to edit a page. +* ``add_page()`` - Allows the user to add a page. We'll describe each one briefly in the following sections. .. note:: - There is nothing special about the filename ``default.py`` exept that - it is a Python module. A project may have many view callables throughout - its codebase in arbitrarily named modules. - Modules implementing view callables often have ``view`` in their name (or - may live in a Python subpackage of your application package named ``views``, - as in our case), but this is only by convention, not a requirement. + There is nothing special about the filename ``default.py`` exept that it is a + Python module. A project may have many view callables throughout its codebase + in arbitrarily named modules. Modules implementing view callables often have + ``view`` in their name (or may live in a Python subpackage of your + application package named ``views``, as in our case), but this is only by + convention, not a requirement. + The ``view_wiki`` view function ------------------------------- @@ -166,13 +172,14 @@ represents the path to our "FrontPage". The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class (instances of which -implement the :class:`pyramid.interfaces.IResponse` interface, like -:class:`pyramid.response.Response` does). It uses the +:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement +the :class:`pyramid.interfaces.IResponse` interface, like +:class:`pyramid.response.Response`). It uses the :meth:`pyramid.request.Request.route_url` API to construct a URL to the ``FrontPage`` page (i.e., ``http://localhost:6543/FrontPage``), and uses it as the "location" of the ``HTTPFound`` response, forming an HTTP redirect. + The ``view_page`` view function ------------------------------- @@ -201,7 +208,7 @@ As a result, the ``content`` variable is now a fully formed bit of HTML containing various view and add links for WikiWords based on the content of our current page object. -We then generate an edit URL because it's easier to do here than in the +We then generate an edit URL, because it's easier to do here than in the template, and we return a dictionary with a number of arguments. The fact that ``view_page()`` returns a dictionary (as opposed to a :term:`response` object) is a cue to :app:`Pyramid` that it should try to use a :term:`renderer` @@ -209,22 +216,22 @@ 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 +If the page does not exist, then we need to handle that by raising a +:class:`pyramid.httpexceptions.HTTPNotFound` to trigger our 404 handling, defined in ``tutorial/views/notfound.py``. .. note:: - Using ``raise`` versus ``return`` with the http exceptions is an important + 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. + registered for handling the ``HTTPNotFound`` exception. Exception views are + only triggered for raised exceptions. If the ``HTTPNotFound`` is returned, + then it has an internal "stock" template that it will use to render itself + as a response. If you aren't seeing your exception view being executed, this + is most likely the problem! See :ref:`special_exceptions_in_callables` for + more information about exception views. + The ``edit_page`` view function ------------------------------- @@ -237,11 +244,11 @@ Here is the code for the ``edit_page`` view function and its decorator: :linenos: :language: python -``edit_page()`` is invoked when a user clicks the "Edit this -Page" button on the view form. It renders an edit form, but it also acts as -the handler for the form it renders. The ``matchdict`` attribute of the -request passed to the ``edit_page`` view will have a ``'pagename'`` key -matching the name of the page the user wants to edit. +``edit_page()`` is invoked when a user clicks the "Edit this Page" button on +the view form. It renders an edit form, but it also acts as the handler for the +form which it renders. The ``matchdict`` attribute of the request passed to the +``edit_page`` view will have a ``'pagename'`` key matching the name of the page +that the user wants to edit. If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), the view grabs the @@ -257,12 +264,13 @@ 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 + 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 ------------------------------ @@ -274,27 +282,26 @@ Here is the code for the ``add_page`` view function and its decorator: :linenos: :language: python -``add_page()`` is invoked when a user clicks on a *WikiWord* which -isn't yet represented as a page in the system. The ``add_link`` function -within the ``view_page`` view generates URLs to this view. -``add_page()`` also acts as a handler for the form that is generated -when we want to add a page object. The ``matchdict`` attribute of the -request passed to the ``add_page()`` view will have the values we need -to construct URLs and find model objects. +``add_page()`` is invoked when a user clicks on a *WikiWord* which isn't yet +represented as a page in the system. The ``add_link`` function within the +``view_page`` view generates URLs to this view. ``add_page()`` also acts as a +handler for the form that is generated when we want to add a page object. The +``matchdict`` attribute of the request passed to the ``add_page()`` view will +have the values we need to construct URLs and find model objects. -The ``matchdict`` will have a ``'pagename'`` key that matches the name of -the page we'd like to add. If our add view is invoked via, -e.g., ``http://localhost:6543/add_page/SomeName``, the value for -``'pagename'`` in the ``matchdict`` will be ``'SomeName'``. +The ``matchdict`` will have a ``'pagename'`` key that matches the name of the +page we'd like to add. If our add view is invoked via, for example, +``http://localhost:6543/add_page/SomeName``, the value for ``'pagename'`` in +the ``matchdict`` will be ``'SomeName'``. If the view execution *is* a result of a form submission (i.e., the expression -``'form.submitted' in request.params`` is ``True``), we grab the page body -from the form data, create a Page object with this page body and the name -taken from ``matchdict['pagename']``, and save it into the database using -``request.dbession.add``. Since we have not yet covered authentication we -don't have a logged-in user to add as the page's ``creator``. Until we -get to that point in the tutorial we'll just assume that all pages are created -by the ``editor`` user so we query that object and set it on ``page.creator``. +``'form.submitted' in request.params`` is ``True``), we grab the page body from +the form data, create a Page object with this page body and the name taken from +``matchdict['pagename']``, and save it into the database using +``request.dbession.add``. Since we have not yet covered authentication, we +don't have a logged-in user to add as the page's ``creator``. Until we get to +that point in the tutorial, we'll just assume that all pages are created by the +``editor`` user. Thus we query for that object, and set it on ``page.creator``. Finally, we redirect the client back to the ``view_page`` view for the newly created page. @@ -308,6 +315,7 @@ in order to satisfy the edit form's desire to have *some* page object exposed as ``page``. :app:`Pyramid` will render the template associated with this view to a response. + Adding templates ================ @@ -317,25 +325,28 @@ 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: +Update ``tutorial/templates/layout.jinja2`` with the following content, as +indicated by the emphasized lines: .. literalinclude:: src/views/tutorial/templates/layout.jinja2 :linenos: :emphasize-lines: 11,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. +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 two placeholders in the layout template where a child + template can override the content. These blocks are named ``subtitle`` (line + 11) and ``content`` (line 36). +- Please refer to the Jinja2_ documentation for more information about template + inheritance. -- We have defined 2 placeholders in the layout template where a child template - can override the content. These blocks are named ``subtitle`` (line 11) and - ``content`` (line 36). -- Please refer to the Jinja2_ documentation for more information about - template inheritance. The ``view.jinja2`` template ---------------------------- @@ -344,23 +355,22 @@ Create ``tutorial/templates/view.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: - :emphasize-lines: 1,3,6 :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 ``subtitle`` block from the base layout to insert the - page name of the page into the page's title (line 3). +- We begin by extending the ``layout.jinja2`` template defined above, which + provides the skeleton of the page (line 1). +- We override the ``subtitle`` block from the base layout, inserting the page + name into the page's title (line 3). - We override the ``content`` block from the base layout to insert our markup - into the body (line 5-18). -- A variable that is replaced with the ``content`` value provided by the view - (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to + into the body (lines 5-18). +- We use a variable that is replaced with the ``content`` value provided by the + view (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to prevent escaping it (e.g., changing ">" to ">"). -- A link that points at the "edit" URL which invokes the ``edit_page`` view for - the page being viewed (line 9). +- We create a link that points at the "edit" URL, which when clicked invokes + the ``edit_page`` view for the requested page (line 9). + The ``edit.jinja2`` template ---------------------------- @@ -369,23 +379,23 @@ Create ``tutorial/templates/edit.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/edit.jinja2 :linenos: - :emphasize-lines: 3,12,14,17 + :emphasize-lines: 1,3,12,14,17 :language: html -This template serves two use-cases. It is used by ``add_page()`` and +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: +containing a form and which provides the following: -- Again, we are extending the ``layout.jinja2`` template which provides - the skeleton of the page (line 1). +- Again, we extend the ``layout.jinja2`` template, which provides the skeleton + of the page (line 1). - Override the ``subtitle`` 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 14). - A submit button that has the name ``form.submitted`` (line 17). +- The form POSTs back to the ``save_url`` argument supplied by the view (line + 12). The view will use the ``body`` and ``form.submitted`` values. -The form POSTs back to the ``save_url`` argument supplied by the view (line -12). The view will use the ``body`` and ``form.submitted`` values. The ``404.jinja2`` template --------------------------- @@ -419,9 +429,9 @@ There are several important things to note about this configuration: ``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. +Finally, we may delete the ``tutorial/templates/mytemplate.jinja2`` template +that was provided by the ``alchemy`` scaffold, as we have created our own +templates for the wiki. .. note:: @@ -431,6 +441,7 @@ 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. + Viewing the application in a browser ==================================== -- cgit v1.2.3 From 492b800ab252521380c465c259a78cc77c934309 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Fri, 26 Feb 2016 22:55:48 -0800 Subject: minor grammar --- docs/tutorials/wiki2/design.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 929bc7806..a57b318ca 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -139,8 +139,8 @@ in the following table: | | - If authentication | | | | | | succeeds, | | | | | | redirect to the | | | | -| | page that we | | | | -| | came from. | | | | +| | page that from | | | | +| | which we came. | | | | | | | | | | | | - If authentication | | | | | | fails, display | | | | -- cgit v1.2.3 From 79054f01a8e2763cb51c1885c60f427802102ac5 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Fri, 26 Feb 2016 22:58:19 -0800 Subject: update authentication (done) - remove highlighting from some code blocks because it didn't make sense and added visual noise and dissonance - minor grammar and syntax --- docs/tutorials/wiki2/authentication.rst | 190 +++++++++++++++++--------------- 1 file changed, 101 insertions(+), 89 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 1b18e5c55..115f7f689 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -5,81 +5,82 @@ 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. +:term:`authorization`. In this section we'll focus solely on the authentication +APIs to add login and logout functionality to our wiki. We will implement authentication with the following steps: -* Add an :term:`authentication policy` and a ``request.user`` computed - property (``security.py``). -* Add routes for /login and /logout (``routes.py``). +* Add 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 +* 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` +The core of :app:`Pyramid` authentication is an :term:`authentication policy` which is used to identify authentication information from a ``request``, -as well as handling the low-level login/logout operations required to -track users across requests (via cookies or headers or whatever else you can +as well as handling the low-level login and logout operations required to +track users across requests (via cookies, headers, or whatever else you can imagine). + Add the authentication policy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a new file ``tutorial/security.py``: +Create a new file ``tutorial/security.py`` with the following content: .. literalinclude:: src/authentication/tutorial/security.py :linenos: - :emphasize-lines: 9,14,21-27 :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. -* 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. +* A new authentication policy named ``MyAuthenticationPolicy``, which is + subclassed from Pyramid's + :class:`pyramid.authentication.AuthTktAuthenticationPolicy`, which tracks the + :term:`userid` using a signed cookie (lines 7-11). +* A ``get_user`` function, which can convert the ``unauthenticated_userid`` + from the policy into a ``User`` object from our database (lines 13-17). +* The ``get_user`` is registered on the request as ``request.user`` to be used + throughout our application as the authenticated ``User`` object for the + logged-in user (line 27). -The logic in this file is a little bit interesting and so we'll go into -detail about what's happening here: +The logic in this file is a little bit interesting, so we'll go into detail +about what's happening here: First, the default authentication policies all provide a method named ``unauthenticated_userid`` which is responsible for the low-level parsing -of the information in the request (cookies, headers, etc). If a ``userid`` -is found then it is returned from this method. This is named -``unauthenticated_userid`` because at the lowest level it knows the value of -the userid in the cookie but it doesn't know if it's actually a user in our +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. +``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 +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. - +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 @@ -96,7 +97,7 @@ the file ``development.ini`` and add the highlighted line below: :lineno-match: :language: ini -Finally best-practices tell us to use a different secret for production so +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 @@ -105,14 +106,15 @@ open ``production.ini`` and add a different secret: :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. +: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: @@ -128,9 +130,9 @@ Open the file ``tutorial/views/default.py`` and fix the following imports: :emphasize-lines: 2,9 :language: python -Only the highlighted lines need to be changed. +Change the two highlighted lines. -Now edit the ``add_page`` view function: +In the same file, now edit the ``add_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py :lines: 62-76 @@ -140,11 +142,11 @@ Now edit the ``add_page`` view function: 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. +If the user either is not logged in or is not in the ``basic`` or ``editor`` +roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden" +response to the user. However, we will hook this later to redirect to the login +page. Also, now that we have ``request.user``, we no longer have to hard-code +the creator as the ``editor`` user, so we can finally drop that hack. Now edit the ``edit_page`` view function: @@ -156,19 +158,21 @@ Now edit the ``edit_page`` view function: 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``. +If the user either is not logged in or the user is not the page's creator +*and* not an ``editor``, then we raise ``HTTPForbidden``. 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! +Now that we've got the ability to detect logged-in users, we need to add the +``/login`` and ``/logout`` views so that they can actually login and logout! -Add routes for /login and /logout -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add routes for ``/login`` and ``/logout`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Go back to ``tutorial/routes.py`` and add these two routes as highlighted: @@ -183,53 +187,57 @@ Go back to ``tutorial/routes.py`` and add these two routes as highlighted: .. literalinclude:: src/authentication/tutorial/routes.py :lines: 6 + :lineno-match: :language: python This is because ``view_page``'s route definition uses a catch-all - "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`) + "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: +Add login, logout, and forbidden views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file ``tutorial/views/auth.py``, and add the following code to it: .. literalinclude:: src/authentication/tutorial/views/auth.py :linenos: :language: python -This code adds 3 new views to application: +This code adds three 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. + The check is done by first finding a ``User`` record in the database, then + using our ``user.check_password`` method to compare the hashed passwords. - If the credentials are valid then we use our authentication policy to - store the user's id in the response using :meth:`pyramid.security.remember`. + 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. + Finally, the user is redirected back to either the page which they were + trying to access (``next``) or the front page as a fallback. This parameter + is used by our forbidden view, as explained below, to finish the login + workflow. -- The ``logout`` view handles requests to /logout by clearing the credentials - using :meth:`pyramid.security.forget` and then redirecting them to the front - page. +- The ``logout`` view handles requests to ``/logout`` by clearing the + credentials using :meth:`pyramid.security.forget`, then redirecting them to + the front page. - The ``forbidden_view`` is registered using the :class:`pyramid.view.forbidden_view_config` decorator. This is a special - :term:`exception view` which is invoked when a + :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. + This view will handle a forbidden error by redirecting the user to + ``/login``. As a convenience, it also sets the ``next=`` query string to the + current URL (the one that is forbidding access). This way, if the user + successfully logs in, they will be sent back to the page which they had been + trying to access. + Add the ``login.jinja2`` template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -242,8 +250,9 @@ Create ``tutorial/templates/login.jinja2`` with the following content: The above template is referenced in the login view that we just added in ``tutorial/views/auth.py``. -Add a "Login" and "Logout" links -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add "Login" and "Logout" links +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Open ``tutorial/templates/layout.jinja2`` and add the following code as indicated by the highlighted lines. @@ -255,9 +264,10 @@ indicated by the highlighted lines. :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. +``tutorial.models.User`` object if the user is authenticated. This check will +make the logout link shown only when the user is logged in, and conversely the +login link is only shown when the user is logged out. + Viewing the application in a browser ------------------------------------ @@ -271,26 +281,28 @@ following URLs, checking that the result is as expected: is executable by any user. - http://localhost:6543/FrontPage invokes the ``view_page`` view of the - ``FrontPage`` page object. There is a "Login" link in the upper right corner. + ``FrontPage`` page object. There is a "Login" link in the upper right corner + while the user is not authenticated, else it is a "Logout" link when the user + is authenticated. - http://localhost:6543/FrontPage/edit_page invokes the edit 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 + ``FrontPage`` object. It is executable by only the ``editor`` user. If a + different user (or the anonymous user) invokes it, then a login form will be + displayed. Supplying the credentials with the username ``editor``, 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. + It is executable by the ``editor`` or ``basic`` user. If a different user (or + the anonymous user) invokes it, then a login form will be displayed. + Supplying the credentials with 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. + user if the page was created by that user in the previous step. If, instead, + the page was created by the ``editor`` user, then the login page should be + shown for the ``basic`` user. - After logging in (as a result of hitting an edit or add page and submitting - the login form with the ``editor`` credentials), we'll see a Logout link in + the 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. -- cgit v1.2.3 From 082d3b2cb9127f8acfd4d081e69c427a37bae91d Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sat, 27 Feb 2016 00:59:17 -0800 Subject: wiki2 authentication bug fix and improvement against timing attack - Bytes type does not have encode method. The expected_hash retrieved from the database is a bytes object. - Use hmac.compare_digest instead of == to avoid timing attacks as a recommended security best practice. See https://www.python.org/dev/peps/pep-0466/ https://bugs.python.org/issue21306 and https://codahale.com/a-lesson-in-timing-attacks/ for details. Note, however, this was not backported to py2.6. For a tutorial, I am OK with stating this will not work on Python 2.6 with a clear warning note at the start of the tutorial and on the authentication step. --- docs/tutorials/wiki2/src/authentication/tutorial/models/user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py index 6fb32a1b2..6499491b2 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py @@ -1,4 +1,5 @@ import bcrypt +import hmac from sqlalchemy import ( Column, Integer, @@ -23,7 +24,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) - return expected_hash == actual_hash + return hmac.compare_digest(expected_hash, actual_hash) return False -- cgit v1.2.3 From f63feb6c3f71f00389ce04f1100ae5675c748557 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sat, 27 Feb 2016 03:47:53 -0800 Subject: grammar fix --- docs/tutorials/wiki2/design.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index a57b318ca..ba25b0697 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -139,8 +139,8 @@ in the following table: | | - If authentication | | | | | | succeeds, | | | | | | redirect to the | | | | -| | page that from | | | | -| | which we came. | | | | +| | page from which | | | | +| | we came. | | | | | | | | | | | | - If authentication | | | | | | fails, display | | | | -- cgit v1.2.3 From f99052e0790b8706161e432da0172b45a805d308 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 01:18:23 -0800 Subject: wiki2 authorization (done) - minor grammar and syntax - align order of bullet points for NewPage and PageResource with code - synch up "viewing app in browser" sections between authentication and authzn --- docs/tutorials/wiki2/authentication.rst | 13 +-- docs/tutorials/wiki2/authorization.rst | 169 +++++++++++++++++--------------- 2 files changed, 98 insertions(+), 84 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 115f7f689..8d9855460 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -288,14 +288,15 @@ following URLs, checking that the result is as expected: - 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, then a login form will be - displayed. Supplying the credentials with the username ``editor``, password - ``editor`` will display the edit page form. + displayed. Supplying the credentials with the username ``editor`` and + password ``editor`` will display the edit page form. - http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by the ``editor`` or ``basic`` user. If a different user (or - the anonymous user) invokes it, then a login form will be displayed. - Supplying the credentials with the username ``basic``, password ``basic``, - will display the edit page form. + It is executable by either the ``editor`` or ``basic`` user. If a different + user (or the anonymous user) invokes it, then a login form will be displayed. + Supplying the credentials with either the username ``editor`` and password + ``editor``, or username ``basic`` and password ``basic``, will display the + edit page form. - http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` user if the page was created by that user in the previous step. If, instead, diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index eb9269dff..a2865d8cd 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -5,39 +5,40 @@ Adding authorization ==================== 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. +went one step further and used the ``request.user`` object to perform some +explicit :term:`authorization` checks. This is fine for a lot of applications, +but :app:`Pyramid` provides some facilities for cleaning this up and decoupling +the constraints from the view function itself. We will implement access control with the following steps: -* Update the :term:`authentication policy` to break down the - :term:`userid` into a list of :term:`principals <principal>` - (``security.py``). +* Update the :term:`authentication policy` to break down the :term:`userid` + into a list of :term:`principals <principal>` (``security.py``). * Define an :term:`authorization policy` for mapping users, resources and permissions (``security.py``). -* Add new :term:`resource` definitions that will be used as the - :term:`context` for the wiki pages (``routes.py``). +* Add new :term:`resource` definitions that will be used as the :term:`context` + for the wiki pages (``routes.py``). * Add an :term:`ACL` to each resource (``routes.py``). * Replace the inline checks on the views with :term:`permission` declarations (``views/default.py``). + Add user principals ------------------- -A :term:`principal` is a level of abstraction on top of the raw -:term:`userid` that describes the user in terms of 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. +A :term:`principal` is a level of abstraction on top of the raw :term:`userid` +that describes the user in terms of its capabilities, roles, or other +identifiers that are easier to generalize. The permissions are then written +against the principals without focusing on the exact user involved. :app:`Pyramid` defines two builtin principals used in every application: :attr:`pyramid.security.Everyone` and :attr:`pyramid.security.Authenticated`. On top of these we have already mentioned the required principals for this -application in the original design. The user has two possible roles: -``editor`` and ``basic``. These will be prefixed by the ``role:`` -string to avoid clasing with any other types of principals. +application in the original design. The user has two possible roles: ``editor`` +or ``basic``. These will be prefixed by the string ``role:`` to avoid clashing +with any other types of principals. -Open the file ``tutorial/security.py`` and edit the following lines: +Open the file ``tutorial/security.py`` and edit it as follows: .. literalinclude:: src/authorization/tutorial/security.py :linenos: @@ -46,19 +47,20 @@ Open the file ``tutorial/security.py`` and edit the following lines: Only the highlighted lines need to be added. -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. +Note that the role comes from the ``User`` object. We also add the ``user.id`` +as a principal for when we want to allow that exact user to edit pages which +they have created. + Add the authorization policy ---------------------------- 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 +:term:`authentication policy`. However, it was not used anywhere, so we'll mention it now. -Open the file ``tutorial/security.py`` and notice the following lines: +In the file ``tutorial/security.py``, notice the following lines: .. literalinclude:: src/authorization/tutorial/security.py :lines: 38-40 @@ -66,36 +68,38 @@ Open the file ``tutorial/security.py`` and notice the following lines: :emphasize-lines: 2 :language: python -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__``. +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__``. + Add resources and ACLs ---------------------- Resources are the hidden gem of :app:`Pyramid`. You've made it! -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. +Every URL in a web application represents a :term:`resource` (the "R" in +Uniform Resource Locator). Often the resource is something in your data model, +but it could also be an abstraction over many models. Our wiki has two resources: -#. 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. +#. A ``NewPage``. Represents a potential ``Page`` that does not exist. Any + logged-in user, having either role of ``basic`` or ``editor``, can create + pages. -#. A ``NewPage``. Represents a potential ``Page`` that does not exist. - Any logged-in user (roles ``basic`` or ``editor``) can create pages. +#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited. + ``editor`` users, as well as the original creator of the ``Page``, may edit + the ``PageResource``. Anyone may view it. .. note:: - 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. + The wiki data model is simple enough that the ``PageResource`` is mostly + redundant with our ``models.Page`` SQLAlchemy class. It is completely valid + to combine these into one class. However, for this tutorial, they are + explicitly separated to make clear the distinction between the parts about + which :app:`Pyramid` cares versus application-defined objects. 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! @@ -109,11 +113,11 @@ Open the file ``tutorial/routes.py`` and edit the following lines: 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: +The ``NewPage`` class has an ``__acl__`` on it that returns a list of mappings +from :term:`principal` to :term:`permission`. This defines *who* can do *what* +with that :term:`resource`. In our case we want to allow only those users with +the principals of either ``role:editor`` or ``role:basic`` to have the +``create`` permission: .. literalinclude:: src/authorization/tutorial/routes.py :lines: 20-32 @@ -121,8 +125,8 @@ have the ``create`` permission: :emphasize-lines: 11,12 :language: python -The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` -route by declaring a ``factory`` on the route: +The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by +declaring a ``factory`` on the route: .. literalinclude:: src/authorization/tutorial/routes.py :lines: 15-16 @@ -130,8 +134,8 @@ route by declaring a ``factory`` on the route: :emphasize-lines: 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. +The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an +actual ``Page`` object to determine *who* can do *what* to the page. .. literalinclude:: src/authorization/tutorial/routes.py :lines: 34-50 @@ -139,8 +143,8 @@ actual ``Page`` object to determine **who** can do **what** to the page. :emphasize-lines: 14-16 :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: +The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and +``edit_page`` routes by declaring a ``factory`` on the routes: .. literalinclude:: src/authorization/tutorial/routes.py :lines: 14-18 @@ -148,14 +152,15 @@ and ``edit_page`` route by declaring a ``factory`` on the routes: :emphasize-lines: 1,4-5 :language: python + Add view permissions -------------------- 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. +``edit_page`` views. Similarly the ``NewPage`` will be the context for the +``add_page`` view. Open the file ``views/default.py``. @@ -167,26 +172,27 @@ First, you can drop a few imports that are no longer necessary: :emphasize-lines: 1 :language: python -Edit the ``view_page`` view to declare the ``view`` permission and remove -the explicit checks within the view: +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: 18-23 :lineno-match: - :emphasize-lines: 2,4 + :emphasize-lines: 1-2,4 :language: python -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. +The work of loading the page has already been done in the factory, so we can +just pull the ``page`` object out of the ``PageResource``, loaded as +``request.context``. Our factory also guarantees we will have a ``Page``, as it +raises the ``HTTPNotFound`` exception if no ``Page`` exists, again simplifying +the view logic. Edit the ``edit_page`` view to declare the ``edit`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py :lines: 38-42 :lineno-match: - :emphasize-lines: 2,4 + :emphasize-lines: 1-2,4 :language: python Edit the ``add_page`` view to declare the ``create`` permission: @@ -194,22 +200,21 @@ Edit the ``add_page`` view to declare the ``create`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py :lines: 52-56 :lineno-match: - :emphasize-lines: 2,4 + :emphasize-lines: 1-2,4 :language: python 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. -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. +The ACLs defined on each :term:`resource` are used by the :term:`authorization +policy` to determine if any :term:`principal` is allowed to have some +:term:`permission`. If this check fails (for example, the user is not logged +in) then an ``HTTPForbidden`` exception will be raised automatically. Thus +we're able to drop those exceptions and checks from the views themselves. +Rather we've defined them in terms of operations on a resource. -The final ``tutorial/views/default.py`` should look like the following: +The final ``views/default.py`` should look like the following: .. literalinclude:: src/authorization/tutorial/views/default.py :linenos: @@ -227,21 +232,29 @@ following URLs, checking that the result is as expected: is executable by any user. - http://localhost:6543/FrontPage invokes the ``view_page`` view of the - ``FrontPage`` page object. + ``FrontPage`` page object. There is a "Login" link in the upper right corner + while the user is not authenticated, else it is a "Logout" link when the user + is authenticated. - http://localhost:6543/FrontPage/edit_page invokes the edit 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. + ``FrontPage`` object. It is executable by only the ``editor`` user. If a + different user (or the anonymous user) invokes it, then a login form will be + displayed. Supplying the credentials with the username ``editor`` and + password ``editor`` will display the edit page form. - http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by only the ``editor`` user. If a different user (or the - anonymous user) invokes it, a login form will be displayed. Supplying the - credentials with the username ``editor``, password ``editor`` will display - the edit page form. + It is executable by either the ``editor`` or ``basic`` user. If a different + user (or the anonymous user) invokes it, then a login form will be displayed. + Supplying the credentials with either the username ``editor`` and password + ``editor``, or username ``basic`` and password ``basic``, will display the + edit page form. + +- http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` + user if the page was created by that user in the previous step. If, instead, + the page was created by the ``editor`` user, then the login page should be + shown for the ``basic`` user. - After logging in (as a result of hitting an edit or add page and submitting - the login form with the ``editor`` credentials), we'll see a Logout link in + the 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. -- cgit v1.2.3 From ee4050ac0a4a60343eb736a90b47113ebfbb60a3 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 01:22:20 -0800 Subject: wiki2 revert unnecessary hmac stuff --- docs/tutorials/wiki2/src/authentication/tutorial/models/user.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py index 6499491b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py @@ -1,5 +1,4 @@ import bcrypt -import hmac from sqlalchemy import ( Column, Integer, @@ -26,5 +25,5 @@ class User(Base): if self.password_hash is not None: expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) - return hmac.compare_digest(expected_hash, actual_hash) + return expected_hash == actual_hash return False -- cgit v1.2.3 From a26a08b92f390aeaa3eb0c39241bb5e76f5a72ea Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 01:29:55 -0800 Subject: apply change to all src/*/user.py --- docs/tutorials/wiki2/src/authorization/tutorial/models/user.py | 2 +- docs/tutorials/wiki2/src/models/tutorial/models/user.py | 2 +- docs/tutorials/wiki2/src/tests/tutorial/models/user.py | 2 +- docs/tutorials/wiki2/src/views/tutorial/models/user.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py index 6fb32a1b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py @@ -23,7 +23,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py index 6fb32a1b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -23,7 +23,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py index 6fb32a1b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py @@ -23,7 +23,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py index 6fb32a1b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py @@ -23,7 +23,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) return expected_hash == actual_hash return False -- cgit v1.2.3 From 606f753fc2ca310debc501874db16b60180773d2 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 02:34:24 -0800 Subject: update tests.rst (done) - minor grammar - mention BaseTest class - clean up test output --- docs/tutorials/wiki2/tests.rst | 52 +++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 667550467..fa9cbe2fc 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -13,38 +13,36 @@ should contain tests for its corresponding module in our application. Each corresponding pair of modules should have the same names, except the test module should have the prefix ``test_``. -We will move parts of ``tests.py`` into appropriate new files in the ``tests`` -subpackage, and add several new tests. - -Start by creating a new directory and a new empty file ``tests/__init__.py``. +Start by deleting ``tests.py``, then create a new directory to contain our new +tests as well as a new empty file ``tests/__init__.py``. .. 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! + It is very important when refactoring a Python module into a package to be + sure to delete the cache files (``.pyc`` files or ``__pycache__`` folders) + sitting around! Python will prioritize the cache files before traversing + into folders, using the old code, and you will wonder why none of your + changes are working! Test the views ============== -We'll create a new ``tests/test_views.py`` file, adding tests for each view -function we previously added to our application. As a result, we'll *delete* -the ``ViewTests`` class that the ``alchemy`` scaffold provided, and add four -other test classes: ``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and -``EditPageTests``. These test the ``view_wiki``, ``view_page``, ``add_page``, -and ``edit_page`` views. +We'll create a new ``tests/test_views.py`` file, adding a ``BaseTest`` class +used as the base for other test classes. Next we'll add tests for each view +function we previously added to our application. We'll add four test classes: +``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``. +These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` +views. Functional tests ================ -We'll test the whole application, covering security aspects that are not -tested in the unit tests, like logging in, logging out, checking that -the ``basic`` user cannot edit pages it didn't create, but the ``editor`` user -can, and so on. +We'll test the whole application, covering security aspects that are not tested +in the unit tests, like logging in, logging out, checking that the ``basic`` +user cannot edit pages that it didn't create but the ``editor`` user can, and +so on. View the results of all our edits to ``tests`` subpackage @@ -67,10 +65,10 @@ follows: .. note:: - 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 + 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``. @@ -88,7 +86,7 @@ On UNIX: On Windows: -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q @@ -96,12 +94,10 @@ The expected result should look like the following: .. code-block:: text - .................... + ..................... ---------------------------------------------------------------------- - Ran 20 tests in 0.524s + Ran 21 tests in 5.117s OK - Process finished with exit code 0 - .. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/ -- cgit v1.2.3 From 3a89ff8ac584f4dcbeca49e1bfbb516006f45446 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 02:38:58 -0800 Subject: update distributing.rst (done) - minor grammar - clean up sdist output --- docs/tutorials/wiki2/distributing.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst index ec90859a9..84e0e6d84 100644 --- a/docs/tutorials/wiki2/distributing.rst +++ b/docs/tutorials/wiki2/distributing.rst @@ -9,13 +9,13 @@ current working directory contains the ``tutorial`` package and the On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py sdist On Windows: -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut> %VENV%\Scripts\python setup.py sdist @@ -26,8 +26,7 @@ The output of such a command will be something like: running sdist # .. more output .. creating dist - tar -cf dist/tutorial-0.0.tar tutorial-0.0 - gzip -f9 dist/tutorial-0.0.tar + Creating tar archive removing 'tutorial-0.0' (and everything under it) Note that this command creates a tarball in the "dist" subdirectory named -- cgit v1.2.3 From 44c087008f39dba616ea62eb413f2fa96a5b6fb3 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Sun, 28 Feb 2016 13:04:30 -0600 Subject: fix some nits --- docs/tutorials/wiki2/authentication.rst | 2 +- docs/tutorials/wiki2/authorization.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 8d9855460..72c11f311 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -207,7 +207,7 @@ Create a new file ``tutorial/views/auth.py``, and add the following code to it: :linenos: :language: python -This code adds three new views to application: +This code adds three new views to the application: - The ``login`` view renders a login form and processes the post from the login form, checking credentials against our ``users`` table in the database. diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index a2865d8cd..1be961f61 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -162,7 +162,7 @@ including the actual ``Page`` model in the ``page_factory``. The ``edit_page`` views. Similarly the ``NewPage`` will be the context for the ``add_page`` view. -Open the file ``views/default.py``. +Open the file ``tutorial/views/default.py``. First, you can drop a few imports that are no longer necessary: @@ -214,7 +214,7 @@ in) then an ``HTTPForbidden`` exception will be raised automatically. Thus we're able to drop those exceptions and checks from the views themselves. Rather we've defined them in terms of operations on a resource. -The final ``views/default.py`` should look like the following: +The final ``tutorial/views/default.py`` should look like the following: .. literalinclude:: src/authorization/tutorial/views/default.py :linenos: -- cgit v1.2.3 From de3062576ce5aa8b2e854626a48e3f5c46b29cb7 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 11:57:14 -0800 Subject: add note about deprecation warnings on py3 --- docs/tutorials/wiki2/tests.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index fa9cbe2fc..dabb73956 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -100,4 +100,11 @@ The expected result should look like the following: OK +.. note:: If you use Python 3 during this tutorial, you will see deprecation + warnings in the output, which we will choose to ignore. In making this + tutorial run on both Python 2 and 3, the authors prioritized simplicity and + focus for the learner over accommodating warnings. In your own app or as + extra credit, you may choose to either drop Python 2 support or hack your + code to work without warnings on both Python 2 and 3. + .. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/ -- cgit v1.2.3 From 3e30040da7c2d5c38b330727b48d9f6b852956d9 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 22:30:22 -0800 Subject: redirect to edit page when user attempts to add page that already exists - update src/*/views/default.py - update src/*/routes.py - write new test - revise docs, double-checking line counts and highlighting --- docs/tutorials/wiki2/authentication.rst | 61 ++++++++++++---------- docs/tutorials/wiki2/authorization.rst | 51 +++++++++--------- docs/tutorials/wiki2/definingviews.rst | 23 +++++--- .../src/authentication/tutorial/views/default.py | 3 ++ .../wiki2/src/authorization/tutorial/routes.py | 8 ++- docs/tutorials/wiki2/src/tests/tutorial/routes.py | 8 ++- .../wiki2/src/tests/tutorial/tests/test_views.py | 12 +++++ .../wiki2/src/views/tutorial/views/default.py | 3 ++ docs/tutorials/wiki2/tests.rst | 2 +- 9 files changed, 108 insertions(+), 63 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 8d9855460..9ee223a50 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -132,34 +132,34 @@ Open the file ``tutorial/views/default.py`` and fix the following imports: Change the two highlighted lines. -In the same file, now edit the ``add_page`` view function: +In the same file, now edit the ``edit_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 62-76 + :lines: 45-60 :lineno-match: - :emphasize-lines: 3-5,10 + :emphasize-lines: 5-7 :language: python Only the highlighted lines need to be changed. -If the user either is not logged in or is not in the ``basic`` or ``editor`` -roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden" -response to the user. However, we will hook this later to redirect to the login -page. Also, now that we have ``request.user``, we no longer have to hard-code -the creator as the ``editor`` user, so we can finally drop that hack. +If the user either is not logged in or the user is not the page's creator +*and* not an ``editor``, then we raise ``HTTPForbidden``. -Now edit the ``edit_page`` view function: +In the same file, now edit the ``add_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 45-60 + :lines: 62-76 :lineno-match: - :emphasize-lines: 5-7 + :emphasize-lines: 3-5,13 :language: python Only the highlighted lines need to be changed. -If the user either is not logged in or the user is not the page's creator -*and* not an ``editor``, then we raise ``HTTPForbidden``. +If the user either is not logged in or is not in the ``basic`` or ``editor`` +roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden" +response to the user. However, we will hook this later to redirect to the login +page. Also, now that we have ``request.user``, we no longer have to hard-code +the creator as the ``editor`` user, so we can finally drop that hack. These simple checks should protect our views. @@ -285,25 +285,28 @@ following URLs, checking that the result is as expected: while the user is not authenticated, else it is a "Logout" link when the user is authenticated. -- http://localhost:6543/FrontPage/edit_page invokes the edit view for the - ``FrontPage`` object. It is executable by only the ``editor`` user. If a - different user (or the anonymous user) invokes it, then a login form will be - displayed. Supplying the credentials with the username ``editor`` and +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. It is executable by only the ``editor`` user. + If a different user (or the anonymous user) invokes it, then a login form + will be displayed. Supplying the credentials with the username ``editor`` and password ``editor`` will display the edit page form. -- http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by either the ``editor`` or ``basic`` user. If a different - user (or the anonymous user) invokes it, then a login form will be displayed. - Supplying the credentials with either the username ``editor`` and password - ``editor``, or username ``basic`` and password ``basic``, will display the - edit page form. +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. It is executable by either the + ``editor`` or ``basic`` user. If a different user (or the anonymous user) + invokes it, then a login form will be displayed. Supplying the credentials + with either the username ``editor`` and password ``editor``, or username + ``basic`` and password ``basic``, will display the edit page form. -- http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` - user if the page was created by that user in the previous step. If, instead, - the page was created by the ``editor`` user, then the login page should be - shown for the ``basic`` user. +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. It is + editable by the ``basic`` user if the page was created by that user in the + previous step. If, instead, the page was created by the ``editor`` user, then + the login page should be shown for the ``basic`` user. - After logging in (as a result of hitting an edit or add page and submitting the login form with the ``editor`` credentials), we'll see a "Logout" link in - the upper right hand corner. When we click it, we're logged out, and - redirected back to the front page. + the upper right hand corner. When we click it, we're logged out, redirected + back to the front page, and a "Login" link is shown in the upper right hand + corner. diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index a2865d8cd..3dec21a23 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -108,7 +108,7 @@ Open the file ``tutorial/routes.py`` and edit the following lines: .. literalinclude:: src/authorization/tutorial/routes.py :linenos: - :emphasize-lines: 1-7,14-50 + :emphasize-lines: 1-11,17- :language: python The highlighted lines need to be edited or added. @@ -120,34 +120,34 @@ the principals of either ``role:editor`` or ``role:basic`` to have the ``create`` permission: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 20-32 + :lines: 30-38 :lineno-match: - :emphasize-lines: 11,12 + :emphasize-lines: 5-9 :language: python The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by declaring a ``factory`` on the route: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 15-16 + :lines: 18-19 :lineno-match: - :emphasize-lines: 2 + :emphasize-lines: 1-2 :language: python The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an actual ``Page`` object to determine *who* can do *what* to the page. .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 34-50 + :lines: 47- :lineno-match: - :emphasize-lines: 14-16 + :emphasize-lines: 5-10 :language: python The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and ``edit_page`` routes by declaring a ``factory`` on the routes: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 14-18 + :lines: 17-21 :lineno-match: :emphasize-lines: 1,4-5 :language: python @@ -236,25 +236,28 @@ following URLs, checking that the result is as expected: while the user is not authenticated, else it is a "Logout" link when the user is authenticated. -- http://localhost:6543/FrontPage/edit_page invokes the edit view for the - ``FrontPage`` object. It is executable by only the ``editor`` user. If a - different user (or the anonymous user) invokes it, then a login form will be - displayed. Supplying the credentials with the username ``editor`` and +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. It is executable by only the ``editor`` user. + If a different user (or the anonymous user) invokes it, then a login form + will be displayed. Supplying the credentials with the username ``editor`` and password ``editor`` will display the edit page form. -- http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by either the ``editor`` or ``basic`` user. If a different - user (or the anonymous user) invokes it, then a login form will be displayed. - Supplying the credentials with either the username ``editor`` and password - ``editor``, or username ``basic`` and password ``basic``, will display the - edit page form. +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. It is executable by either the + ``editor`` or ``basic`` user. If a different user (or the anonymous user) + invokes it, then a login form will be displayed. Supplying the credentials + with either the username ``editor`` and password ``editor``, or username + ``basic`` and password ``basic``, will display the edit page form. -- http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` - user if the page was created by that user in the previous step. If, instead, - the page was created by the ``editor`` user, then the login page should be - shown for the ``basic`` user. +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. It is + editable by the ``basic`` user if the page was created by that user in the + previous step. If, instead, the page was created by the ``editor`` user, then + the login page should be shown for the ``basic`` user. - After logging in (as a result of hitting an edit or add page and submitting the login form with the ``editor`` credentials), we'll see a "Logout" link in - the upper right hand corner. When we click it, we're logged out, and - redirected back to the front page. + the upper right hand corner. When we click it, we're logged out, redirected + back to the front page, and a "Login" link is shown in the upper right hand + corner. diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index e48980dc8..b0cbe7dc4 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -124,7 +124,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- The highlighted lines need to be added or edited. @@ -277,7 +277,7 @@ The ``add_page`` view function Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 58-70 + :lines: 58- :lineno-match: :linenos: :language: python @@ -294,6 +294,10 @@ page we'd like to add. If our add view is invoked via, for example, ``http://localhost:6543/add_page/SomeName``, the value for ``'pagename'`` in the ``matchdict`` will be ``'SomeName'``. +Next a check is performed to determine whether the ``Page`` already exists in +the database. If it already exists, then the client is redirected to the +``edit_page`` view, else we continue to the next check. + If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), we grab the page body from the form data, create a Page object with this page body and the name taken from @@ -452,13 +456,18 @@ each of the following URLs, checking that the result is as expected: - http://localhost:6543/ invokes the ``view_wiki`` view. This always redirects to the ``view_page`` view of the ``FrontPage`` page object. -- http://localhost:6543/FrontPage invokes the ``view_page`` view of the front - page object. +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` page object. + +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. -- http://localhost:6543/FrontPage/edit_page invokes the edit view for the - front page object. +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. -- http://localhost:6543/add_page/SomePageName invokes the add view for a page. +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. - To generate an error, visit http://localhost:6543/foobars/edit_page which will generate a ``NoResultFound: No row was found for one()`` error. You'll diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py index ffe967f6e..1b071434c 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -65,6 +65,9 @@ def add_page(request): if user is None or user.role not in ('editor', 'basic'): raise HTTPForbidden pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + return HTTPFound(location=next_url) if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py index c7c3a2120..f0a8b7f96 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py @@ -1,4 +1,7 @@ -from pyramid.httpexceptions import HTTPNotFound +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPFound, +) from pyramid.security import ( Allow, Everyone, @@ -19,6 +22,9 @@ def includeme(config): def new_page_factory(request): pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + raise HTTPFound(location=next_url) return NewPage(pagename) class NewPage(object): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py index c7c3a2120..f0a8b7f96 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/routes.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py @@ -1,4 +1,7 @@ -from pyramid.httpexceptions import HTTPNotFound +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPFound, +) from pyramid.security import ( Allow, Everyone, @@ -19,6 +22,9 @@ def includeme(config): def new_page_factory(request): pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + raise HTTPFound(location=next_url) return NewPage(pagename) class NewPage(object): 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 5253183df..2c945ab33 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -98,6 +98,18 @@ class AddPageTests(BaseTest): from tutorial.views.default import add_page return add_page(request) + def test_it_pageexists(self): + from ..models import Page + from ..routes import NewPage + request = testing.DummyRequest({'form.submitted': True, + 'body': 'Hello yo!'}, + dbsession=self.session) + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') + self._callFUT(request) + pagecount = self.session.query(Page).filter_by(name='AnotherPage').count() + self.assertGreater(pagecount, 0) + def test_it_notsubmitted(self): from ..routes import NewPage request = dummy_request(self.session) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index c1d402f6a..bb6300b75 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -58,6 +58,9 @@ def edit_page(request): @view_config(route_name='add_page', renderer='../templates/edit.jinja2') def add_page(request): pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + return HTTPFound(location=next_url) if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index dabb73956..54aea28c6 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -96,7 +96,7 @@ The expected result should look like the following: ..................... ---------------------------------------------------------------------- - Ran 21 tests in 5.117s + Ran 22 tests in 5.320s OK -- cgit v1.2.3