pyramid_blogr tutorial

Pyramid_blogr is aimed to introduce readers to basic concepts of the Pyramid Web Framework and web application development.

This tutorial is based on project template for 1.10-branch of the framework.

Note

This tutorial is meant for Python 2.7 and Python 3.3+ versions of the language. We strongly recommend using Python 3.x.

Contents

What you will know after you finish this tutorial

This tutorial is really simple, but it should give you enough of a head start to writing applications using the Pyramid Web Framework. By the end of it, you should have a basic understanding of templating, working with databases, using URL routes to launch business logic (views), authentication, authorization, using a form library, and usage and pagination of our blog entries.

Pyramid_blogr makes some initial assumptions:

  • We will use Pyramid’s alchemy scaffold with SQLAlchemy as its ORM layer.
  • Jinja2 templates will be our choice for templating engine.
  • URL dispatch will be the default way for our view resolution.
  • A single user in the database will be created during the setup phase.
  • We will perform simple authentication of the user.
  • The authenticated user will be authorized to make blog entries.
  • The blog entries will be listed from newest to oldest.
  • We will use the paginate package for pagination.
  • The WTForms form library will provide form validation.

This tutorial was originally created by Marcin Lulek, developer, freelancer and founder of app enlight.

Start the tutorial.

Improve this tutorial

There may be mistakes in this tutorial, and we always appreciate pull requests to improve this tutorial. Please use the issue tracker on GitHub or submit pull requests to the project through GitHub. Please see contributing.md for details.

Requirements

Note

This section was modified from the original Pyramid Quick Tutorial Requirements.

Let’s get our tutorial environment setup. Most of the setup work is in standard Python development practices (install Python, make an isolated environment, and setup packaging tools.)

Note

Pyramid encourages standard Python development practices with packaging tools, virtual environments, logging, and so on. There are many variations, implementations, and opinions across the Python community. For consistency, ease of documentation maintenance, and to minimize confusion, the Pyramid documentation has adopted specific conventions.

This Pyramid Blogr Tutorial is based on:

  • Python 3.3. Pyramid fully supports Python 3.2+ and Python 2.6+. This tutorial uses Python 3.3 but runs fine under Python 2.7.
  • pyvenv. We believe in virtual environments. For this tutorial, we use Python 3’s built-in solution, the pyvenv command. For Python 2.7, you can install virtualenv.
  • pip. We use pip for package management.
  • cookiecutter. We use cookiecutter for fresh project creation.
  • Workspaces, projects, and packages. Our home directory will contain a tutorial workspace with our Python virtual environment(s) and Python projects (a directory with packaging information and Python packages of working code.)
  • UNIX commands. Commands in this tutorial use UNIX syntax and paths. Windows users should adjust commands accordingly.

Note

Pyramid was one of the first web frameworks to fully support Python 3 in October 2011.

Steps

  1. Install Python 3.3 or greater
  2. Workspace and project directory structure
  3. Set an environment variable
  4. Create a virtual environment
  5. Install Cookiecutter
Install Python 3.3 or greater

Download the latest standard Python 3.3+ release (not development release) from python.org.

Windows and Mac OS X users can download and run an installer.

Windows users should also install the Python for Windows extensions. Carefully read the README.txt file at the end of the list of builds, and follow its directions. Make sure you get the proper 32- or 64-bit build and Python version.

Linux users can either use their package manager to install Python 3.3+ or may build Python 3.3+ from source.

Workspace and project directory structure

We will arrive at a directory structure of workspace -> project -> package, with our workspace named blogr_tutorial. The following tree diagram shows how this will be structured and where our virtual environment will reside as we proceed through the tutorial:

~/
└── projects/
    └── blogr_tutorial/
        ├── env/
        └── pyramid_blogr/
            ├── CHANGES.txt
            ├── MANIFEST.in
            ├── README.txt
            ├── development.ini
            ├── production.ini
            ├── pytest.ini
            ├── pyramid_blogr/
            │   ├── __init__.py
            │   ├── models
            │   │   ├── __init__.py
            │   │   ├── meta.py
            │   │   └── mymodel.py
            │   ├── routes.py
            │   ├── scripts/
            │   │   ├── __init__.py
            │   │   └── initializedb.py
            │   ├── static/
            │   │   ├── pyramid-16x16.png
            │   │   ├── pyramid.png
            │   │   └── theme.css
            │   ├── templates/
            │   │   ├── 404.jinja2
            │   │   ├── layout.jinja2
            │   │   └── mytemplate.jinja2
            │   ├── tests.py
            │   └── views
            │   │   ├── __init__.py
            │   │   ├── default.py
            │   │   └── notfound.py
            └── setup.py

For Linux, the commands to do so are as follows:

# Mac and Linux
$ cd ~
$ mkdir -p projects/blogr_tutorial
$ cd projects/blogr_tutorial

For Windows:

# Windows
c:\> cd \
c:\> mkdir projects\blogr_tutorial
c:\> cd projects\blogr_tutorial

In the above figure, your user home directory is represented by ~. In your home directory, all of your projects are in the projects directory. This is a general convention not specific to Pyramid that many developers use. Windows users will do well to use c:\ as the location for projects in order to avoid spaces in any of the path names.

Next within projects is your workspace directory, here named blogr_tutorial. A workspace is a common term used by integrated development environments (IDE) like PyCharm and PyDev that stores isolated Python environments (virtualenvs) and specific project files and repositories.

Set an environment variable

This tutorial will refer frequently to the location of the virtual environment. We set an environment variable to save typing later.

# Mac and Linux
$ export VENV=~/projects/blogr_tutorial/env

# Windows
# TODO: This command does not work
c:\> set VENV=c:\projects\blogr_tutorial\env
Create a virtual environment

Warning

The current state of isolated Python environments using pyvenv on Windows is suboptimal in comparison to Mac and Linux. See http://stackoverflow.com/q/15981111/95735 for a discussion of the issue and PEP 453 for a proposed resolution.

pyvenv is a tool to create isolated Python 3 environments, each with its own Python binary and independent set of installed Python packages in its site directories. Let’s create one, using the location we just specified in the environment variable.

# Mac and Linux
$ pyvenv $VENV

# Windows
c:\> c:\Python33\python -m venv %VENV%

See also

See also Python 3’s venv module. For instructions to set up your Python environment for development on UNIX or Windows, or using Python 2, see Pyramid’s Before You Install.

Install Cookiecutter

Cookiecutter A command-line utility that creates projects from cookiecutters (project templates), e.g. creating a Python package project from a Python package project template.

# Mac and Linux
$ $VENV/bin/pip install cookiecutter

# Windows
c:\\> %VENV%\\Scripts\\pip install cookiecutter

With the requirements satisfied, you may continue to the next step in this tutorial 1. Create your pyramid_blogr project structure.

1. Create your pyramid_blogr project structure

Note

At the time of writing, 1.9 was the most recent stable version of Pyramid. You can use newer versions of Pyramid, but there may be some slight differences in default project templates.

When we installed Pyramid, several scripts were installed in your virtual environment including:

  • cookiecutter - Used to create a new project and directory structures from Pyramid scaffolds (project templates) provided in separate repository.
  • pserve - Used to start a WSGI server.

Using the cookiecutter script, we will create our project using the alchemy scaffold, which will provide SQLAlchemy as our default ORM layer.

$ $VENV/bin/cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout 1.10-branch

When asked for the project name enter pyramid_blogr, repo_name can be the same.

When asked for the template system pick jinja2 - the default option.

When asked for the backend system pick sqlalchemy.

We will end up with the directory pyramid_blogr which should have the structure as explained below.

├── CHANGES.txt
├── .coveragerc
├── development.ini <- configuration for local development
├── .gitignore
├── MANIFEST.in     <- manifest for python packaging tools
├── production.ini  <- configuration for production deployments
├── pytest.ini      <- configuration for test runner
├── README.txt
├── setup.py        <- python package build script
└── pyramid_blogr/  <- your application src dir
    ├── __init__.py <- main file that will configure and return WSGI application
    ├── alembic      <- model definitions aka data sources (often RDBMS or noSQL)
    │   ├── versions
    │   │   └── README.txt
    │   ├── env.py
    │   └── script.py.mako
    ├── models      <- model definitions aka data sources (often RDBMS or noSQL)
    │   ├── __init__.py
    │   ├── meta.py
    │   └── mymodel.py
    ├── routes.py
    ├── scripts/    <- util Python scripts
    │   ├── __init__.py
    │   └── initializedb.py
    ├── static/     <- usually css, js, images
    │   ├── pyramid-16x16.png
    │   ├── pyramid.png
    │   └── theme.css
    ├── templates/  <- template files
    │   ├── 404.jinja2
    │   ├── layout.jinja2
    │   └── mytemplate.jinja2
    ├── tests.py    <- tests
    └── views       <- views aka business logic
        ├── __init__.py
        ├── default.py
        └── notfound.py

Adding dependencies to the project

Since Pyramid tries its best to be a non-opinionated solution, we will have to decide which libraries we want for form handling and template helpers. For this tutorial, we will use the WTForms library and webhelpers2 package.

To make them dependencies of our application, we need to open the setup.py file and extend the requires section with additional packages. In the end, it should look like the following.

requires = [
    'plaster_pastedeploy',
    'pyramid',
    'pyramid_jinja2',
    'pyramid_debugtoolbar',
    'waitress',
    'alembic',
    'pyramid_retry',
    'pyramid_tm',
    'SQLAlchemy',
    'transaction',
    'zope.sqlalchemy',
    'wtforms==2.2.1',  # form library
    'webhelpers2==2.0',  # various web building related helpers
    'paginate==0.5.6', # pagination helpers
    'paginate_sqlalchemy==0.3.0'
]

Now we can setup our application for development and add it to our environment path. Change directory to the root of our project where setup.py lives, and install the dependencies in setup.py with the following commands.

$ cd pyramid_blogr
$ $VENV/bin/pip install -e .

Warning

Don’t forget to add the period (.) after the -e switch.

This will install all the requirements for our application, making it importable into our Python environment.

Another side effect of this command is that our environment gained another command called initialize_pyramid_blogr_db, we will use it to create and populate the database from the models we will create in a moment. This script will also create the default user for our application.

Running our application

To visit our application, we need to use a WSGI server that will start serving the content to the browser with following command.

$ $VENV/bin/pserve --reload development.ini

This will launch an instance of a WSGI server (waitress by default) that will run both your application code and static files. The file development.ini is used to provide all the configuration details. The --reload parameter tells the server to restart our application every time its code changes. This is a very useful setting for fast development and testing changes to our app with live reloading.

$ $VENV/bin/pserve --reload development.ini

Starting subprocess with file monitor
Starting server in PID 8517.
serving on http://0.0.0.0:6543

You can open a web browser and visit the URL http://localhost:6543/ to see how our application looks.

Unfortunately you will see something like the following instead of a webpage.

Pyramid is having a problem using your SQL database.  The problem...

This is where the initialize_pyramid_blogr_db command comes into play; but before we run it, we need to create our application models.

Stop the WSGI server with CTRL-C, then proceed to the next section in the tutorial, 2. Create database models.

2. Create database models

At this point we should create our models. In a nutshell, models represent data and its underlying storage mechanisms in an application.

We will use a relational database and SQLAlchemy’s ORM layer to access our data.

Create and edit models/user.py

Our application will consist of two tables:

  • users - stores all users for our application
  • entries - stores our blog entries

We should assume that our users might use some Unicode characters, so we need to import the Unicode datatype from SQLAlchemy. We will also need a DateTime field to timestamp our blog entries.

Let’s first create models/user.py.

$ touch pyramid_blogr/models/user.py

Add the following code to models/user.py.

1
2
3
4
5
6
7
8
9
import datetime #<- will be used to set default dates on models
from pyramid_blogr.models.meta import Base  #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
    Column,
    Integer,
    Unicode,     #<- will provide Unicode field
    UnicodeText, #<- will provide Unicode text field
    DateTime,    #<- time abstraction field
)

Make a copy of models/user.py as models/blog_record.py. We will need these imports in both modules.

$ cp pyramid_blogr/models/user.py pyramid_blogr/models/blog_record.py

The alchemy scaffold in Pyramid provides an example model class MyModel that we don’t need, as well as code that creates an index, so let’s remove the file models/mymodel.py.

$ rm pyramid_blogr/models/mymodel.py

Now our project structure should look like this.

pyramid_blogr/
......
├── models      <- model definitions aka data sources (often RDBMS or noSQL)
│     ├── __init__.py <- database engine initialization
│     ├── blog_record.py
│     ├── meta.py <- database sqlalchemy metadata object
│     └── user.py
......
Database session management

Note

To learn how to use SQLAlchemy, please consult its Object Relational Tutorial.

If you are new to SQLAlchemy or ORM’s, you are probably wondering what the code from models/__init__.py does.

To explain we need to start reading it from the includeme() part.

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

def includeme(config):
    """
    Initialize the model for a Pyramid app.

    Activate this setup using ``config.include('pyramid_blogr.models')``.

    """
    settings = config.get_settings()
    settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'

    # use pyramid_tm to hook the transaction lifecycle to the request
    config.include('pyramid_tm')

    # use pyramid_retry to retry a request when transient exceptions occur
    config.include('pyramid_retry')

    session_factory = get_session_factory(get_engine(settings))
    config.registry['dbsession_factory'] = session_factory

    # make request.dbsession available for use in Pyramid
    config.add_request_method(
        # r.tm is the transaction manager used by pyramid_tm
        lambda r: get_tm_session(session_factory, r.tm),
        'dbsession',
        reify=True
    )

The first line defines a special function called includeme it will be picked up by pyramid on runtime and will ensure that on every request, the request object will have a dbsession propery attached that will point to SQLAlchemy’s session object.

The function also imports pyramid_tm - it is Pyramid’s transaction manager that will be attached to our request object as tm property, it will be managing our dbsession objects behavior.

We will use it to interact with the database and persist our changes to the database. It is thread-safe, meaning that it will handle multiple requests at the same time in a safe way, and our code from different views will not impact other requests. It will also open and close database connections for us transparently when needed.

What does transaction manager do?

WHOA THIS SOUNDS LIKE SCARY MAGIC!!

Note

It’s not.

OK, so while it might sound complicated, in practice it’s very simple and saves a developer a lot of headaches with managing transactions inside an application.

Here’s how the transaction manager process works:

  • A transaction is started when a browser request invokes our view code.
  • Some operations take place; for example, database rows are inserted or updated in our datastore.
    • If everything went fine, we don’t need to commit our transaction explictly; the transaction manager will do this for us.
    • If some unhandled exception occurred, we usually want to roll back all the changes and queries that were sent to our datastore; the transaction manager will handle this for us.

What are the implications of this?

Imagine you have an application that sends a confirmation email every time a user registers. A user, Nephthys, inputs the data to register, and we send Nephthys a nice welcome email and maybe an activation link, but during registration flow, something unexpected happens and the code errors out.

It is very common in this situation that the user would get a welcome email, but in reality their profile was never persisted in the database. With packages like pyramid_mailer it is perfectly possible to delay email sending until after the user’s information is successfully saved in the database.

Nice, huh?

Although this is a more advanced topic not covered in depth in this tutorial, the most simple explanation is that the transaction manager will make sure our data gets correctly saved if everything went smoothly, and if an error occurs then our datastore modifications are rolled back.

Adding model definitions

Note

This will make the application error out and prevent it from starting until we reach the last point of the current step and fix imports in other files. It’s perfectly normal, so don’t worry about immediate errors.

We will need two declarations of models that will replace the MyModel class that was created when we scaffolded our project.

After the import part in models/user.py add the following.

12
13
14
15
16
17
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Unicode(255), unique=True, nullable=False)
    password = Column(Unicode(255), nullable=False)
    last_logged = Column(DateTime, default=datetime.datetime.utcnow)

After the import part in models/blog_record.py add the following.

12
13
14
15
16
17
18
class BlogRecord(Base):
    __tablename__ = 'entries'
    id = Column(Integer, primary_key=True)
    title = Column(Unicode(255), unique=True, nullable=False)
    body = Column(UnicodeText, default=u'')
    created = Column(DateTime, default=datetime.datetime.utcnow)
    edited = Column(DateTime, default=datetime.datetime.utcnow)

Now it’s time to update our models/__init__.py to include our models. This is especially handy because it ensures that SQLAlchemy mappers will pick up all of our model classes and functions, like create_all, and that the models will do what you expect.

Add these imports to the file (remember to also remove the MyModel import).

 6
 7
 8
 9
10
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
from .user import User
from .blog_record import BlogRecord

Update database initialization script

It’s time to update our database initialization script to mirror the changes in our models package.

Open scripts/initialize_db.py. This is the file that actually gets executed when we run initialize_pyramid_blogr_db.

We want to replace the following bits:

def setup_models(dbsession):
    """
    Add or update models / fixtures in the database.

    """
    model = models.mymodel.MyModel(name='one', value=1)
    dbsession.add(model)

with this:

10
11
12
13
14
15
16
17
def setup_models(dbsession):
    """
    Add or update models / fixtures in the database.

    """

    model = models.user.User(name=u'admin', password=u'admin')
    dbsession.add(model)

When you initialize a fresh database, this will populate it with a single user, with both login and unencrypted password equal to admin.

Warning

This is just a tutorial example and production code should utilize passwords hashed with a strong one-way encryption function. You can use a package like passlib for this purpose. This is covered later in the tutorial.

The last step to get the application running is to change views/default.py MyModel class into out User model.

 9
10
11
12
13
14
15
16
@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
    try:
        query = request.dbsession.query(models.User)
        one = query.filter(models.User.name == 'one').first()
    except DBAPIError:
        return Response(db_err_msg, content_type='text/plain', status=500)
    return {'one': one, 'project': 'pyramid_blogr'}

Our application should start again if we try running the server.

$ $VENV/bin/pserve --reload development.ini

If you visit the URL http://0.0.0.0:6543 then you should see a “Pyramid is having a problem …” error message.

In case you have problems starting the application, you can see complete source code of the files we modifed below.

models/__init__.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from sqlalchemy import engine_from_config
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
import zope.sqlalchemy

# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
from .user import User
from .blog_record import BlogRecord

# 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('pyramid_blogr.models')``.

    """
    settings = config.get_settings()
    settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'

    # use pyramid_tm to hook the transaction lifecycle to the request
    config.include('pyramid_tm')

    # use pyramid_retry to retry a request when transient exceptions occur
    config.include('pyramid_retry')

    session_factory = get_session_factory(get_engine(settings))
    config.registry['dbsession_factory'] = session_factory

    # make request.dbsession available for use in Pyramid
    config.add_request_method(
        # r.tm is the transaction manager used by pyramid_tm
        lambda r: get_tm_session(session_factory, r.tm),
        'dbsession',
        reify=True
    )

models/user.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import datetime #<- will be used to set default dates on models
from pyramid_blogr.models.meta import Base  #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
    Column,
    Integer,
    Unicode,     #<- will provide Unicode field
    UnicodeText, #<- will provide Unicode text field
    DateTime,    #<- time abstraction field
)


class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Unicode(255), unique=True, nullable=False)
    password = Column(Unicode(255), nullable=False)
    last_logged = Column(DateTime, default=datetime.datetime.utcnow)

models/blog_record.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import datetime #<- will be used to set default dates on models
from pyramid_blogr.models.meta import Base  #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
    Column,
    Integer,
    Unicode,     #<- will provide Unicode field
    UnicodeText, #<- will provide Unicode text field
    DateTime,    #<- time abstraction field
)


class BlogRecord(Base):
    __tablename__ = 'entries'
    id = Column(Integer, primary_key=True)
    title = Column(Unicode(255), unique=True, nullable=False)
    body = Column(UnicodeText, default=u'')
    created = Column(DateTime, default=datetime.datetime.utcnow)
    edited = Column(DateTime, default=datetime.datetime.utcnow)

scripts/initialize_db.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import argparse
import sys

from pyramid.paster import bootstrap, setup_logging
from sqlalchemy.exc import OperationalError

from .. import models


def setup_models(dbsession):
    """
    Add or update models / fixtures in the database.

    """

    model = models.user.User(name=u'admin', password=u'admin')
    dbsession.add(model)


def parse_args(argv):
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'config_uri',
        help='Configuration file, e.g., development.ini',
    )
    return parser.parse_args(argv[1:])


def main(argv=sys.argv):
    args = parse_args(argv)
    setup_logging(args.config_uri)
    env = bootstrap(args.config_uri)

    try:
        with env['request'].tm:
            dbsession = env['request'].dbsession
            setup_models(dbsession)
    except OperationalError:
        print('''
Pyramid is having a problem using your SQL database.  The problem
might be caused by one of the following things:

1.  You may need to initialize your database tables with `alembic`.
    Check your README.txt for description and try to run it.

2.  Your database server may not be running.  Check that the
    database server referred to by the "sqlalchemy.url" setting in
    your "development.ini" file is running.
            ''')

__init__.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pyramid.config import Configurator


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    with Configurator(settings=settings) as config:
        config.include('.models')
        config.include('pyramid_jinja2')
        config.include('.routes')
        config.scan()
    return config.make_wsgi_app()

views/default.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pyramid.view import view_config
from pyramid.response import Response

from sqlalchemy.exc import DBAPIError

from .. import models


@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
    try:
        query = request.dbsession.query(models.User)
        one = query.filter(models.User.name == 'one').first()
    except DBAPIError:
        return Response(db_err_msg, content_type='text/plain', status=500)
    return {'one': one, 'project': 'pyramid_blogr'}


db_err_msg = """\
Pyramid is having a problem using your SQL database.  The problem
might be caused by one of the following things:

1.  You may need to initialize your database tables with `alembic`.
    Check your README.txt for descriptions 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.
"""

If our application starts correctly, you should run the initialize_pyramid_blogr_db command to generate database migrations.

# run this in the root of the project directory
$ $VENV/bin/alembic -c development.ini revision --autogenerate -m "init"

This will generate database migration file out of your models in pyramid_blogr/alembic/versions/ directory.

Example output:

2018-12-23 15:49:16,408 INFO  [alembic.runtime.migration:117][MainThread] Context impl SQLiteImpl.
2018-12-23 15:49:16,408 INFO  [alembic.runtime.migration:122][MainThread] Will assume non-transactional DDL.
2018-12-23 15:49:16,423 INFO  [alembic.autogenerate.compare:115][MainThread] Detected added table 'entries'
2018-12-23 15:49:16,423 INFO  [alembic.autogenerate.compare:115][MainThread] Detected added table 'users'
  Generating /home/ergo/workspace/pyramid_blogr/pyramid_blogr/alembic/versions/20181223_5899f27f265f.py ... done

Generated migration file might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
"""init

Revision ID: 5899f27f265f
Revises: 
Create Date: 2018-12-23 16:39:13.677058

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '5899f27f265f'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('entries',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('title', sa.Unicode(length=255), nullable=False),
    sa.Column('body', sa.UnicodeText(), nullable=True),
    sa.Column('created', sa.DateTime(), nullable=True),
    sa.Column('edited', sa.DateTime(), nullable=True),
    sa.PrimaryKeyConstraint('id', name=op.f('pk_entries')),
    sa.UniqueConstraint('title', name=op.f('uq_entries_title'))
    )
    op.create_table('users',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.Unicode(length=255), nullable=False),
    sa.Column('password', sa.Unicode(length=255), nullable=False),
    sa.Column('last_logged', sa.DateTime(), nullable=True),
    sa.PrimaryKeyConstraint('id', name=op.f('pk_users')),
    sa.UniqueConstraint('name', name=op.f('uq_users_name'))
    )
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('users')
    op.drop_table('entries')
    # ### end Alembic commands ###

Now you can run the migration against your database.

$ $VENV/bin/alembic -c development.ini upgrade head

Example output:

2018-12-23 15:51:49,238 INFO  [alembic.runtime.migration:117][MainThread] Context impl SQLiteImpl.
2018-12-23 15:51:49,238 INFO  [alembic.runtime.migration:122][MainThread] Will assume non-transactional DDL.
2018-12-23 15:51:49,239 INFO  [alembic.runtime.migration:327][MainThread] Running upgrade  -> 4325dedd2673, init

Since your database has all the necessary user and blog tables you can populate it with admin user.

$ $VENV/bin/initialize_pyramid_blogr_db development.ini

If you start the application you should be able to see index page.

Next 3. Application routes.

3. Application routes

This is the point where we want to define our routes that will be used to map view callables to request paths.

URL dispatch provides a simple way to map URLs to view code using a simple pattern matching language.

Our application will consist of a few sections:

  • index page that will list all of our sorted blog entries
  • a sign in/sign out section that will be used for authentication
  • a way to create and edit our blog posts

Our URLs will look like the following.

To sign in users:

/sign/in

When a user visits http://somedomain.foo/sign/in, the view callable responsible for signing in the user based on POST variables from the request will be executed.

To sign out users:

/sign/out

The index page (this was already defined via the alchemy scaffold we used earlier, under the name “home”):

/

Create new, or edit existing blog entries:

/blog/{action}

You probably noticed that this URL appears unusual. The {action} part in the matching pattern determines that this part is dynamic, so our URL could look like any of the following:

/blog/create
/blog/edit
/blog/foobar

This single route could map to different views.

Finally a route used to view our blog entries:

/blog/{id:\d+}/{slug}

This route constists of two dynamic parts, {id:\d+} and {slug}.

The :\d+ pattern means that the route will only match digits. So an URL where the first dynamic part consists of only digits like the following would be matched:

/blog/156/Some-blog-entry

But the below example would not be matched, because the first dynamic part contains a non-digit character:

/blog/something/Some-blog-entry

Adding routes to the application configuration

Let’s add our routes to the configurator immediately after the home route in our routes.py at the root of our project.

1
2
3
4
5
6
def includeme(config):
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home', '/')
    config.add_route('blog', '/blog/{id:\d+}/{slug}')
    config.add_route('blog_action', '/blog/{action}')
    config.add_route('auth', '/sign/{action}')

Now we are ready to develop views.

Next: 4. Initial views.

4. Initial views

Now it’s time to create our views files and add our view callables.

Every view will be decorated with a @view_config decorator.

@view_config will configure our Pyramid application by telling it how to correlate our view callables with routes, and set some restrictions on specific view resolution mechanisms. It gets picked up when config.scan() is called in our __init__.py, and all of our views are registered with our app.

Note

You could explictly configure your application with the config.add_view() method, but the approach with @view_config and config.scan() is often more convenient.

Edit our views files

Let’s make some stubs for our views. We will populate them with actual code in later chapters.

Open views/default.py and replace the current @view_config decorator and the def my_view() function with the following.

1
2
3
4
5
6
7
from pyramid.view import view_config


@view_config(route_name='home',
             renderer='pyramid_blogr:templates/index.jinja2')
def index_page(request):
    return {}

Here @view_config takes two parameters that will register our index_page callable in Pyramid’s registry, specifying the route that should be used to match this view. We also specify the renderer that will be used to transform the data which the view returns into a response suitable for the client.

The template location is specified using the asset location format, which is in the form of package_name:path_to_template, or by specifying relative path to the template.

Note

It also easy to add your own custom renderer, or use an add-on package like pyramid_mako.

The renderer is picked up automatically by specifying the file extension, like asset.jinja2/asset.jinja2 or when you provide a name, such as for the string/json renderer.

Pyramid provides a few renderers including:
  • jinja2 templates (by external package)
  • mako templates (by external package)
  • chameleon templates (by external package)
  • string output
  • json encoder

Create new file views/blog.py and add the following contents:

1
2
3
4
5
6
7
from pyramid.view import view_config


@view_config(route_name='blog',
             renderer='pyramid_blogr:templates/view_blog.jinja2')
def blog_view(request):
    return {}

This registers blog_view with a route named 'blog' using the view_blog.jinja2 template as the response.

The next views we should create are views that will handle creation and updates to our blog entries.

10
11
12
13
@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_create(request):
    return {}

Notice that there is a new keyword introduced to the @view_config decorator. The purpose of match_param is to tell Pyramid which view callable to use when the dynamic part of the route {action} is matched. For example, the above view will be launched for the URL /blog/create.

Next we add the view for the URL /blog/edit.

16
17
18
19
@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_update(request):
    return {}

Note

Every view can be decorated unlimited times with different parameters passed to @view_config.

Now switch back to views/default.py, and add the following.

10
11
12
13
14
@view_config(route_name='auth', match_param='action=in', renderer='string',
             request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    return {}

These routes will handle user authentication and logout. They do not use a template because they will just perform HTTP redirects.

Note that this view is decorated more than once. It also introduces one new parameter. request_method restricts view resolution to a specific request method, or in this example, to just POST requests. This route will not be reachable with GET requests.

Note

If you navigate your browser directly to /sign/in, you will get a 404 page because this view is not matched for GET requests.

Content of views files

Here’s how our views files look at this point.

views/blog.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from pyramid.view import view_config


@view_config(route_name='blog',
             renderer='pyramid_blogr:templates/view_blog.jinja2')
def blog_view(request):
    return {}


@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_create(request):
    return {}


@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_update(request):
    return {}
views/default.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pyramid.view import view_config


@view_config(route_name='home',
             renderer='pyramid_blogr:templates/index.jinja2')
def index_page(request):
    return {}


@view_config(route_name='auth', match_param='action=in', renderer='string',
             request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    return {}

views/__init__.py is currently an empty file.

At this point we can start implementing our view code.

Next: 5. Blog models and views.

5. Blog models and views

Models

Since our stubs are in place, we can start developing blog related code.

Let’s start with models. Now that we have them, we can create some service classes, and implement some methods that we will use in our views and templates.

Create a new directory services/. Inside of that, create two new empty files, __init__.py and blog_record.py. These files comprise a new subpackage.

We will leave __init__.py empty.

Open services/blog_record.py. Import some helper modules to generate our slugs, add pagination, and print nice dates. They all come from the excellent webhelpers2 package. Add the following imports at the top of blog_record.py.

1
2
3
import sqlalchemy as sa
from paginate_sqlalchemy import SqlalchemyOrmPage #<- provides pagination
from ..models.blog_record import BlogRecord

Next we need to create our BlogRecordService class with methods as follows.

 6
 7
 8
 9
10
11
class BlogRecordService(object):

    @classmethod
    def all(cls, request):
        query = request.dbsession.query(BlogRecord)
        return query.order_by(sa.desc(BlogRecord.created))

The method all will return a query object that can return an entire dataset when needed.

The query object will sort the rows by date in descending order.

13
14
15
16
    @classmethod
    def by_id(cls, _id, request):
        query = request.dbsession.query(BlogRecord)
        return query.get(_id)

The above method will return either a single blog entry by id or the None object if nothing is found.

18
19
20
21
22
23
24
25
26
27
28
29
30
    @classmethod
    def get_paginator(cls, request, page=1):
        query = request.dbsession.query(BlogRecord)
        query = query.order_by(sa.desc(BlogRecord.created))
        query_params = request.GET.mixed()

        def url_maker(link_page):
            # replace page param with values generated by paginator
            query_params['page'] = link_page
            return request.current_route_url(_query=query_params)

        return SqlalchemyOrmPage(query, page, items_per_page=5,
                                 url_maker=url_maker)

The get_paginator method will return a paginator object that returns the entries from a specific “page” of records from a database resultset. It will add LIMIT and OFFSET clauses to our query based on the value of items_per_page and the current page number.

Paginator uses the wrapper SqlalchemyOrmPage which will attempt to generate a paginator with links. Link URLs will be constructed using the function url_maker which uses the request object to generate a new URL from the current one, replacing the page query parameter with the new value.

Your project structure should look like this at this point.

pyramid_blogr/
......
├── services      <- they query the models for data
│     ├── __init__.py
│     └── blog_record.py
......

Now it is time to move up to the parent directory, to add imports and properties to models/blog_record.py.

10
11
from webhelpers2.text import urlify #<- will generate slugs
from webhelpers2.date import distance_of_time_in_words #<- human friendly dates

Then add the following method to the class BlogRecord.

22
23
24
    @property
    def slug(self):
        return urlify(self.title)

This property of entry instance will return nice slugs for us to use in URLs. For example, pages with the title of “Foo Bar Baz” will have URLs of “Foo-Bar-Baz”. Also non-Latin characters will be approximated to their closest counterparts.

Next add another method.

26
27
28
29
    @property
    def created_in_words(self):
        return distance_of_time_in_words(self.created,
                                         datetime.datetime.utcnow())

This property will return information about when a specific entry was created in a human-friendly form, like “2 days ago”.

Index view

First lets add our BlogRecord service to imports in views/default.py.

2
from ..services.blog_record import BlogRecordService

Now it’s time to implement our actual index view by modifying our view index_page.

 5
 6
 7
 8
 9
10
@view_config(route_name='home',
             renderer='pyramid_blogr:templates/index.jinja2')
def index_page(request):
    page = int(request.params.get('page', 1))
    paginator = BlogRecordService.get_paginator(request, page)
    return {'paginator': paginator}

We first retrieve from the URL’s request object the page number that we want to present to the user. If the page number is not present, it defaults to 1.

The paginator object returned by BlogRecord.get_paginator will then be used in the template to build a nice list of entries.

Note

Everything we return from our views in dictionaries will be available in templates as variables. So if we return {'foo':1, 'bar':2}, then we will be able to access the variables inside the template directly as foo and bar.

Index view template

First rename mytemplate.jinja2 to index.jinja2.

Lets now go over the contents of the file layout.jinja2, a template file that will store a “master” template that from which other view templates will inherit. This template will contain the page header and footer shared by all pages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!DOCTYPE html>
<html lang="{{request.locale_name}}">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="pyramid web application">
    <meta name="author" content="Pylons Project">
    <link rel="shortcut icon" href="{{request.static_url('pyramid_blogr:static/pyramid-16x16.png')}}">

    <title>Cookiecutter Starter project for the Pyramid Web Framework</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

    <!-- Custom styles for this scaffold -->
    <link href="{{request.static_url('pyramid_blogr:static/theme.css')}}" rel="stylesheet">

    <!-- HTML5 shiv and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
      <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></script>
    <![endif]-->
  </head>

  <body>

    <div class="starter-template">
      <div class="container">
        <div class="row">
          <div class="col-md-2">
            <img class="logo img-responsive" src="{{request.static_url('pyramid_blogr:static/pyramid.png') }}" alt="pyramid web framework">
          </div>
          <div class="col-md-10">
            {% block content %}
                <p>No content</p>
            {% endblock content %}
          </div>
        </div>
        <div class="row">
          <div class="links">
            <ul>
              <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
              <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
              <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
            </ul>
          </div>
        </div>
        <div class="row">
          <div class="copyright">
            Copyright &copy; Pylons Project
          </div>
        </div>
      </div>
    </div>


    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
  </body>
</html>

Note

The request object is always available inside your templates namespace.

Inside your template, you will notice that we use the method request.static_url which will generate correct links to your static assets. This is handy when building apps using URL prefixes.

In the middle of the template, you will also notice the tag {% block content %}. After we render a template that inherits from our layout file, this is the place where our index template (or others for other views) will appear.

Now let’s open index.jinja2 and put this content in it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{% extends "layout.jinja2" %}

{% block content %}

    {% if paginator.items %}

        <h2>Blog entries</h2>

        <ul>
            {% for entry in paginator.items %}
                <li>
                    <a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">
                        {{ entry.title }}
                    </a>
                </li>
            {% endfor %}
        </ul>

        {{ paginator.pager() |safe }}

    {% else %}

        <p>No blog entries found.</p>

    {% endif %}

    <p><a href="{{ request.route_url('blog_action',action='create') }}">
        Create a new blog entry</a></p>

{% endblock %}

This template extends or inherits from layout.jinja2, which means that its contents will be wrapped by the layout provided by the parent template.

{{paginator.pager()}} will print nice paginator links. It will only show up if you have more than 5 blog entries in the database. The |safe filter marks the output as safe HTML so jinja2 knows it doesn’t need to escape any HTML code outputted by the pager class.

request.route_url is used to generate links based on routes defined in our project. For example:

{{request.route_url('blog_action',action='create')}} -> /blog/create

If you start the application, you should see new index page showing that no blog entries found. It should also feature “create blog entru” link that will give a 404 response for now.

Blog view

Time to update our blog view. Near the top of views/blog.py, let’s add the following imports

2
3
4
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
from ..models.blog_record import BlogRecord
from ..services.blog_record import BlogRecordService

Those HTTP exceptions will be used to perform redirects inside our apps.

  • HTTPFound will return a 302 HTTP code response. It can accept an argument location that will add a Location: header for the browser. We will perform redirects to other pages using this exception.
  • HTTPNotFound on the other hand will just make the server serve a standard 404 Not Found response.

Continue editing views/blog.py, by modifying the blog_view view.

 7
 8
 9
10
11
12
13
14
@view_config(route_name='blog',
             renderer='pyramid_blogr:templates/view_blog.jinja2')
def blog_view(request):
    blog_id = int(request.matchdict.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    return {'entry': entry}

This view is very simple. First we get the id variable from our route. It will be present in the matchdict property of the request object. All of our defined route arguments will end up there.

After we get the entry id, it will be passed to the BlogRecord class method by_id() to fetch a specific blog entry. If it’s found, we return the database row for the template to use, otherwise we present the user with a standard 404 response.

Blog view template

Create a new file to use as the template for blog article presentation, named view_blog.jinja2, with the following content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{% extends "pyramid_blogr:templates/layout.jinja2" %}

{% block content %}
    <h1>{{ entry.title }}</h1>
    <hr/>
    <p>{{ entry.body }}</p>
    <hr/>
    <p>Created <strong title="{{ entry.created }}">
        {{ entry.created_in_words }}</strong> ago</p>

    <p><a href="{{ request.route_url('home') }}">Go Back</a> ::
        <a href="{{ request.route_url('blog_action', action='edit',
        _query={'id':entry.id}) }}">Edit entry</a>

    </p>
{% endblock %}

The _query argument introduced here for the URL generator is a list of key/value tuples that will be used to append as GET (query) parameters, separated by a question mark “?”. In this case, the URL will be appended by the GET query string of ?id=X, where the value of X is the id of the specific blog post to retrieve.

If you start the application now, you will get an empty welcome page stating that “No blog entries are found”.

File contents

Here are the entire file contents up to this point, except for those already provided in their entirety.

services/blog_record.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import sqlalchemy as sa
from paginate_sqlalchemy import SqlalchemyOrmPage #<- provides pagination
from ..models.blog_record import BlogRecord


class BlogRecordService(object):

    @classmethod
    def all(cls, request):
        query = request.dbsession.query(BlogRecord)
        return query.order_by(sa.desc(BlogRecord.created))

    @classmethod
    def by_id(cls, _id, request):
        query = request.dbsession.query(BlogRecord)
        return query.get(_id)

    @classmethod
    def get_paginator(cls, request, page=1):
        query = request.dbsession.query(BlogRecord)
        query = query.order_by(sa.desc(BlogRecord.created))
        query_params = request.GET.mixed()

        def url_maker(link_page):
            # replace page param with values generated by paginator
            query_params['page'] = link_page
            return request.current_route_url(_query=query_params)

        return SqlalchemyOrmPage(query, page, items_per_page=5,
                                 url_maker=url_maker)
models/blog_record.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import datetime #<- will be used to set default dates on models
from pyramid_blogr.models.meta import Base  #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
    Column,
    Integer,
    Unicode,     #<- will provide Unicode field
    UnicodeText, #<- will provide Unicode text field
    DateTime,    #<- time abstraction field
)
from webhelpers2.text import urlify #<- will generate slugs
from webhelpers2.date import distance_of_time_in_words #<- human friendly dates


class BlogRecord(Base):
    __tablename__ = 'entries'
    id = Column(Integer, primary_key=True)
    title = Column(Unicode(255), unique=True, nullable=False)
    body = Column(UnicodeText, default=u'')
    created = Column(DateTime, default=datetime.datetime.utcnow)
    edited = Column(DateTime, default=datetime.datetime.utcnow)

    @property
    def slug(self):
        return urlify(self.title)

    @property
    def created_in_words(self):
        return distance_of_time_in_words(self.created,
                                         datetime.datetime.utcnow())
views/default.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from pyramid.view import view_config
from ..services.blog_record import BlogRecordService


@view_config(route_name='home',
             renderer='pyramid_blogr:templates/index.jinja2')
def index_page(request):
    page = int(request.params.get('page', 1))
    paginator = BlogRecordService.get_paginator(request, page)
    return {'paginator': paginator}


@view_config(route_name='auth', match_param='action=in', renderer='string',
             request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    return {}
views/blog.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
from ..models.blog_record import BlogRecord
from ..services.blog_record import BlogRecordService


@view_config(route_name='blog',
             renderer='pyramid_blogr:templates/view_blog.jinja2')
def blog_view(request):
    blog_id = int(request.matchdict.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    return {'entry': entry}


@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_create(request):
    return {}


@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_update(request):
    return {}

Next: 6. Adding and editing blog entries.

6. Adding and editing blog entries

Form handling with WTForms library

For form validation and creation, we will use a very friendly and easy to use form library called WTForms. First we need to define our form schemas that will be used to generate form HTML and validate values of form fields.

In the root of our application, let’s create the file forms.py with following content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from wtforms import Form, StringField, TextAreaField, validators
from wtforms import IntegerField
from wtforms.widgets import HiddenInput

strip_filter = lambda x: x.strip() if x else None

class BlogCreateForm(Form):
    title = StringField('Title', [validators.Length(min=1, max=255)],
                        filters=[strip_filter])
    body = TextAreaField('Contents', [validators.Length(min=1)],
                         filters=[strip_filter])

class BlogUpdateForm(BlogCreateForm):
    id = IntegerField(widget=HiddenInput())

We create a simple filter that will be used to remove all the whitespace from the beginning and end of our input.

Then we create a BlogCreateForm class that defines two fields:

  • title has a label of “Title” and a single validator that will check the length of our trimmed data. The title length needs to be in the range of 1-255 characters.
  • body has a label of “Contents” and a validator that requires its length to be at least 1 character.

Next is the BlogUpdateForm class that inherits all the fields from BlogCreateForm, and adds a new hidden field called id. id will be used to determine which entry we want to update.

Create blog entry view

Now that our simple form definition is ready, we can actually write our view code.

Lets start by importing our freshly created form schemas to views/blog.py.

4
5
from ..services.blog_record import BlogRecordService
from ..forms import BlogCreateForm, BlogUpdateForm

Add the emphasized line as indicated.

Next we implement a view callable that will handle new entries for us.

18
19
20
21
22
23
24
25
26
27
@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_create(request):
    entry = BlogRecord()
    form = BlogCreateForm(request.POST)
    if request.method == 'POST' and form.validate():
        form.populate_obj(entry)
        request.dbsession.add(entry)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form, 'action': request.matchdict.get('action')}

Only the emphasized lines need to be added or edited.

The view callable does the following:

  • Create a new fresh entry row and form object from BlogCreateForm.
  • The form will be populated via POST, if present.
  • If the request method is POST, the form gets validated.
  • If the form is valid, our form sets its values to the model instance, and adds it to the database session.
  • Redirect to the index page.

If the form doesn’t validate correctly, the view result is returned, and a standard HTML response is returned instead. The form markup will have error messages included.

Create update entry view

The following view will handle updates to existing blog entries.

30
31
32
33
34
35
36
37
38
39
40
41
42
43
@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_update(request):
    blog_id = int(request.params.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    form = BlogUpdateForm(request.POST, entry)
    if request.method == 'POST' and form.validate():
        del form.id  # SECURITY: prevent overwriting of primary key
        form.populate_obj(entry)
        return HTTPFound(
            location=request.route_url('blog', id=entry.id,slug=entry.slug))
    return {'form': form, 'action': request.matchdict.get('action')}

Only the emphasized lines need to be added or edited.

Here’s what the view does:

  • Fetch the blog entry from the database based in the id query parameter.
  • Show a 404 Not Found page if the requested record is not present.
  • Create the form object, populating it from the POST parameters or from the actual blog entry, if we haven’t POSTed any values yet.

Note

This approach ensures our form is always populated with the latest data from the database, or if the submission is not valid then the values we POSTed in our last request will populate the form fields.

  • If the form is valid, our form sets its values to the model instance.
  • Redirect to the blog page.

Plase notice the line:

39
        del form.id  # SECURITY: prevent overwriting of primary key

Note

IMPORTANT SECURITY INFORMATION

For the sake of tutorial, we showcase how to create inheritable form schemas where creation view might be have different fields than update/moderation view, in this case this is id field.

One important thing to remember is that populate_obj will overwrite ALL fields that are present in the Form object, so you need to remember to remove the read-only fields or better yet implement your own population method on your model. As of today WTForms does not provide any built-in field exclusion mechanisms.

For convenience, here is the complete views/blog.py thusfar, with added and edited lines emphasized.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
from ..models.blog_record import BlogRecord
from ..services.blog_record import BlogRecordService
from ..forms import BlogCreateForm, BlogUpdateForm


@view_config(route_name='blog',
             renderer='pyramid_blogr:templates/view_blog.jinja2')
def blog_view(request):
    blog_id = int(request.matchdict.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    return {'entry': entry}


@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_create(request):
    entry = BlogRecord()
    form = BlogCreateForm(request.POST)
    if request.method == 'POST' and form.validate():
        form.populate_obj(entry)
        request.dbsession.add(entry)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form, 'action': request.matchdict.get('action')}


@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2')
def blog_update(request):
    blog_id = int(request.params.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    form = BlogUpdateForm(request.POST, entry)
    if request.method == 'POST' and form.validate():
        del form.id  # SECURITY: prevent overwriting of primary key
        form.populate_obj(entry)
        return HTTPFound(
            location=request.route_url('blog', id=entry.id,slug=entry.slug))
    return {'form': form, 'action': request.matchdict.get('action')}

Create a template for creating and editing blog entries

The final step is to add a template that will present users with the form to create and edit entries. Let’s call it templates/edit_blog.jinja2 and place the following code as its content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{% extends "pyramid_blogr:templates/layout.jinja2" %}

{% block content %}
    <form action="{{request.route_url('blog_action',action=action)}}" method="post" class="form">
        {% if action =='edit' %}
            {{ form.id() }}
        {% endif %}

        {% for error in form.title.errors %}
            <div class="error">{{ error }}</div>
        {% endfor %}

        <div class="form-group">
            <label for="title">{{ form.title.label }}</label>
            {{ form.title(class_='form-control') }}
        </div>

        {% for error in form.body.errors %}
            <div class="error">{{ error }}</div>
        {% endfor %}

        <div class="form-group">
            <label for="body">{{ form.body.label }}</label>
            {{ form.body(class_='form-control') }}
        </div>
        <div class="form-group">
            <label></label>
            <button type="submit" class="btn btn-default">Submit</button>
        </div>


    </form>
    <p><a href="{{ request.route_url('home') }}">Go Back</a></p>
{% endblock %}

Our template knows if we are creating a new row or updating an existing one based on the action variable value. If we are editing an existing row, the template will add a hidden field named id that holds the id of the entry that is being updated.

If the form doesn’t validate, then the field errors properties will contain lists of errors for us to present to the user.

Now launch the app, and visit http://localhost:6543/ and you will notice that you can now create and edit blog entries.

Note

Because WTForms form instances are iterable, you can easily write a template function that will iterate over its fields and auto generate dynamic HTML for each of them.

Now it is time to work towards securing them.

Next: 7. Authorization.

7. Authorization

At this point we have a fully working application, but you may have noticed that anyone can alter our entries. We should change that by introducing user authorization, where we assign security statements to resources (e.g., blog entries) describing the permissions required to perform an operation (e.g., add or edit a blog entry).

For the sake of simplicity, in this tutorial we will assume that every user can edit every blog entry as long as they are signed in to our application.

Pyramid provides some ready-made policies for this, as well as mechanisms for writing custom policies.

We will use the policies provided by the framework:

  • AuthTktAuthenticationPolicy

    Obtains user data from a Pyramid “auth ticket” cookie.

  • ACLAuthorizationPolicy

    An authorization policy which consults an ACL object attached to a context to determine authorization information about a principal or multiple principals.

OK, so the description for ACLAuthorizationPolicy has a lot of scary words in it, but in practice it’s a simple concept that allows for great flexibility when defining permission systems.

The policy basically checks if a user has a permission to the specific context of a view based on Access Control Lists.

What does this mean? What is a context?

A context could be anything. Imagine you are building a forum application, and you want to add a feature where only moderators will be able to edit a specific topic in a specific forum. In this case, our context would be the forum object; it would have info attached to it about who has specific permissions to this resource.

Or something simpler, who can access admin pages? In this case, a context would be an arbitrary object that has information attached to it about who is an administrator of the site.

How does this relate to our application?

Since our application does not track who owns blog entries, we will assume the latter scenario: any authenticated (logged in) user has authorization to administer the blog entries. We will make the most trivial context factory object. As its name implies, the factory will return the context object (in our case, an arbitrary class). It will say that everyone logged in to our application can create and edit blog entries.

Create a context factory

In the root of our application package, let’s create a new file called security.py with the following content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pyramid.security import Allow, Everyone, Authenticated


class BlogRecordFactory(object):
    __acl__ = [(Allow, Everyone, 'view'),
               (Allow, Authenticated, 'create'),
               (Allow, Authenticated, 'edit'), ]

    def __init__(self, request):
        pass

This is the object that was mentioned a moment ago, a context factory. It’s not tied to any specific entity in a database, and it returns an __acl__ property which says that everyone has a 'view' permission, and users that are logged in also have 'create' and 'edit' permissions.

Create authentication and authorization policies

Now it’s time to tell Pyramid about the policies we want to register with our application.

Let’s open our configuration __init__.py at the root of our project, and add the following imports as indicated by the emphasized lines.

1
2
3
from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

Now it’s time to update our configuration. We need to create our policies, and pass them to the configurator. Add or edit the emphasized lines.

 9
10
11
12
13
14
15
    authentication_policy = AuthTktAuthenticationPolicy('somesecret')
    authorization_policy = ACLAuthorizationPolicy()
    with Configurator(settings=settings,
                      authentication_policy=authentication_policy,
                      authorization_policy=authorization_policy) as config:
        config.include('.models')
        config.include('pyramid_jinja2')

The string “somesecret” passed into the policy will be a secret string used for cookie signing, so that our authentication cookie is secure.

The last thing we need to add is to assign our context factory to our routes. We want this to be the route responsible for entry creation and updates. Modify the following emphasized lines.

1
2
3
4
5
6
7
def includeme(config):
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home', '/')
    config.add_route('blog', '/blog/{id:\d+}/{slug}')
    config.add_route('blog_action', '/blog/{action}',
                     factory='pyramid_blogr.security.BlogRecordFactory')
    config.add_route('auth', '/sign/{action}')

Now for the finishing touch. We set “create” and “edit” permissions on our views. Open views/blog.py, and change our @view_config decorators as shown by the following emphasized lines.

18
19
20
@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2',
             permission='create')
31
32
33
@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2',
             permission='create')

Current state of our application

For convenience here are the two files you have edited in their entirety up to this point (security.py was already rendered above).

__init__.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    authentication_policy = AuthTktAuthenticationPolicy('somesecret')
    authorization_policy = ACLAuthorizationPolicy()
    with Configurator(settings=settings,
                      authentication_policy=authentication_policy,
                      authorization_policy=authorization_policy) as config:
        config.include('.models')
        config.include('pyramid_jinja2')
        config.include('.routes')
        config.scan()
    return config.make_wsgi_app()
views/blog.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
from ..models.blog_record import BlogRecord
from ..services.blog_record import BlogRecordService
from ..forms import BlogCreateForm, BlogUpdateForm


@view_config(route_name='blog',
             renderer='pyramid_blogr:templates/view_blog.jinja2')
def blog_view(request):
    blog_id = int(request.matchdict.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    return {'entry': entry}


@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2',
             permission='create')
def blog_create(request):
    entry = BlogRecord()
    form = BlogCreateForm(request.POST)
    if request.method == 'POST' and form.validate():
        form.populate_obj(entry)
        request.dbsession.add(entry)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form, 'action': request.matchdict.get('action')}


@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2',
             permission='create')
def blog_update(request):
    blog_id = int(request.params.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    form = BlogUpdateForm(request.POST, entry)
    if request.method == 'POST' and form.validate():
        del form.id  # SECURITY: prevent overwriting of primary key
        form.populate_obj(entry)
        return HTTPFound(
            location=request.route_url('blog', id=entry.id,slug=entry.slug))
    return {'form': form, 'action': request.matchdict.get('action')}

Now if you try to visit the links to create or update entries, you will see that they respond with a 403 HTTP status because Pyramid detects that there is no user object that has edit or create permissions.

Our views are secured!

Next: 8. Authentication.

8. Authentication

Great, we secured our views, but now no one can add new entries to our application. The finishing touch is to implement our authentication views.

Create a sign-in/sign-out form

First we need to add a login form to our existing index.jinja2 template as shown by the emphasized lines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% extends "layout.jinja2" %}

{% block content %}

    {% if request.authenticated_userid %}
        Welcome <strong>{{request.authenticated_userid}}</strong> ::
        <a href="{{request.route_url('auth',action='out')}}">Sign Out</a>
    {% else %}
        <form action="{{request.route_url('auth',action='in')}}" method="post" class="form-inline">
            <div class="form-group">
                <input type="text" name="username" class="form-control" placeholder="Username">
            </div>
            <div class="form-group">
                <input type="password" name="password" class="form-control" placeholder="Password">
            </div>
            <div class="form-group">
                <input type="submit" value="Sign in" class="btn btn-default">
            </div>
        </form>
    {% endif %}

    {% if paginator.items %}

Now the template first checks if we are logged in. If we are logged in, it greets the user and presents a sign-out link. Otherwise we are presented with the sign-in form.

Update User model

Now it’s time to update our User model.

Lets update our model with two methods: verify_password to check user input with a password associated with the user instance, and by_name that will fetch our user from the database, based on login.

Add the following method to our User class in models/user.py.

19
20
    def verify_password(self, password):
        return self.password == password

We also need to create the UserService class in a new file services/user.py.

1
2
3
4
5
6
7
8
from ..models.user import User


class UserService(object):

    @classmethod
    def by_name(cls, name, request):
        return request.dbsession.query(User).filter(User.name == name).first()

Warning

In a real application, verify_password should use some strong one-way hashing algorithm like bcrypt or pbkdf2. Use a package like passlib which uses strong hashing algorithms for hashing of passwords.

Update views

The final step is to update the view that handles authentication.

First we need to add the following import to views/default.py.

1
2
3
4
5
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from ..services.user import UserService
from ..services.blog_record import BlogRecordService

Those functions will return HTTP headers which are used to set our AuthTkt cookie (from AuthTktAuthenticationPolicy) in the user’s browser. remember is used to set the current user, whereas “forget” is used to sign out our user.

Now we have everything ready to implement our actual view.

18
19
20
21
22
23
24
25
26
27
28
29
30
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    username = request.POST.get('username')
    if username:
        user = UserService.by_name(username, request=request)
        if user and user.verify_password(request.POST.get('password')):
            headers = remember(request, user.name)
        else:
            headers = forget(request)
    else:
        headers = forget(request)
    return HTTPFound(location=request.route_url('home'), headers=headers)

This is a very simple view that checks if a database row with the supplied username is present in the database. If it is, a password check against the username is performed. If the password check is successful, then a new set of headers (which is used to set the cookie) is generated and passed back to the client on redirect. If the username is not found, or if the password doesn’t match, then a set of headers meant to remove the cookie (if any) is issued.

Current state of our application

For convenience here are the files you edited in their entirety (services/user.py was already rendered above).

templates/index.jinja2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{% extends "layout.jinja2" %}

{% block content %}

    {% if request.authenticated_userid %}
        Welcome <strong>{{request.authenticated_userid}}</strong> ::
        <a href="{{request.route_url('auth',action='out')}}">Sign Out</a>
    {% else %}
        <form action="{{request.route_url('auth',action='in')}}" method="post" class="form-inline">
            <div class="form-group">
                <input type="text" name="username" class="form-control" placeholder="Username">
            </div>
            <div class="form-group">
                <input type="password" name="password" class="form-control" placeholder="Password">
            </div>
            <div class="form-group">
                <input type="submit" value="Sign in" class="btn btn-default">
            </div>
        </form>
    {% endif %}

    {% if paginator.items %}

        <h2>Blog entries</h2>

        <ul>
            {% for entry in paginator.items %}
                <li>
                    <a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">
                        {{ entry.title }}
                    </a>
                </li>
            {% endfor %}
        </ul>

        {{ paginator.pager() |safe }}

    {% else %}

        <p>No blog entries found.</p>

    {% endif %}

    <p><a href="{{ request.route_url('blog_action',action='create') }}">
        Create a new blog entry</a></p>

{% endblock %}
models/user.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import datetime #<- will be used to set default dates on models
from pyramid_blogr.models.meta import Base  #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
    Column,
    Integer,
    Unicode,     #<- will provide Unicode field
    UnicodeText, #<- will provide Unicode text field
    DateTime,    #<- time abstraction field
)


class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Unicode(255), unique=True, nullable=False)
    password = Column(Unicode(255), nullable=False)
    last_logged = Column(DateTime, default=datetime.datetime.utcnow)

    def verify_password(self, password):
        return self.password == password
views/default.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from ..services.user import UserService
from ..services.blog_record import BlogRecordService


@view_config(route_name='home',
             renderer='pyramid_blogr:templates/index.jinja2')
def index_page(request):
    page = int(request.params.get('page', 1))
    paginator = BlogRecordService.get_paginator(request, page)
    return {'paginator': paginator}


@view_config(route_name='auth', match_param='action=in', renderer='string',
             request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    username = request.POST.get('username')
    if username:
        user = UserService.by_name(username, request=request)
        if user and user.verify_password(request.POST.get('password')):
            headers = remember(request, user.name)
        else:
            headers = forget(request)
    else:
        headers = forget(request)
    return HTTPFound(location=request.route_url('home'), headers=headers)

Voilà!

You can now sign in and out to add and edit blog entries using the login admin with password admin (this user was added to the database during the initialize_db step). But we have a few more steps to complete this project.

Next: 9. Registration.

9. Registration

Now we have a basic functioning application, but we have only one hardcoded administrator user that can add and edit blog entries. We can provide a registration page for new users, too.

Then we need to to provide a quality hashing solution so we can store secure password hashes instead of clear text. This functionality will be provided by passlib.

Create the registration class, route, and form

We should create a form to handle registration requests. Let’s open forms.py at the root of our project, and edit an import at the top of the files and add a new form class at the end as indicated by the emphasized lines.

1
2
from wtforms import Form, StringField, TextAreaField, validators
from wtforms import IntegerField, PasswordField
17
18
19
20
class RegistrationForm(Form):
    username = StringField('Username', [validators.Length(min=1, max=255)],
                           filters=[strip_filter])
    password = PasswordField('Password', [validators.Length(min=3)])
Our second step will be adding a new route that handles user registration in
routes.py file as shown by the emphasized line.
7
8
    config.add_route('auth', '/sign/{action}')
    config.add_route('register', '/register')

We should add a link to the registration page in our templates/index.jinja2 template so we can easily navigate to it as shown by the emphasized line.

19
20
21
        </form>
        <a href="{{request.route_url('register')}}">Register here</a>
    {% endif %}

Create the registration view

At this point we have the form object and routing set up. We are missing a related view, model, and template code. Let us move forward with the view code in views/default.py.

First we need to import our form definition user model at the top of the file as shown by the emphasized lines.

5
6
7
from ..services.blog_record import BlogRecordService
from ..forms import RegistrationForm
from ..models.user import User

Then we can start implementing our view logic by adding the following lines to the end of the file.

34
35
36
37
38
39
@view_config(route_name='register',
             renderer='pyramid_blogr:templates/register.jinja2')
def register(request):
    form = RegistrationForm(request.POST)
    if request.method == 'POST' and form.validate():
        new_user = User(name=form.username.data)
40
41
42
        request.dbsession.add(new_user)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form}

Next let’s create a new registration template called templates/register.jinja2 with the following content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{% extends "pyramid_blogr:templates/layout.jinja2" %}

{% block content %}
    <h1>Register</h1>

    <form action="{{request.route_url('register')}}" method="post" class="form">

        {% for error in form.username.errors %}
            <div class="error">{{ error }}</div>
        {% endfor %}

        <div class="form-group">
            <label for="title">{{form.username.label}}</label>
            {{form.username(class_='form-control')}}
        </div>

        {% for error in form.password.errors %}
            <div class="error">{{error}}</div>
        {% endfor %}

        <div class="form-group">
            <label for="body">{{form.password.label}}</label>
            {{form.password(class_='form-control')}}
        </div>
        <div class="form-group">
            <label></label>
            <button type="submit" class="btn btn-default">Submit</button>
        </div>


    </form>
    <p><a href="{{request.route_url('home')}}">Go Back</a></p>
{% endblock %}

Hashing passwords

Our users can now register themselves and are stored in the database using unencrypted passwords (which is a really bad idea).

This is exactly where passlib comes into play. We should add it to our project’s requirements in setup.py at the root of our project.

requires = [
    ...
    'paginate==0.5.6', # pagination helpers
    'paginate_sqlalchemy==0.2.0',
    'passlib'
]

Now we can run either command pip install passlib or python setup.py develop to pull in the new dependency of our project. Password hashing can now be implemented in our User model class.

We need to import the hash context object from passlib and alter the User class to contain new versions of methods verify_password and set_password. Open models/user.py and edit it as indicated by the emphasized lines.

11
12
13
14
15
16
17
18
19
20
21
from passlib.apps import custom_app_context as blogger_pwd_context


class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Unicode(255), unique=True, nullable=False)
    password = Column(Unicode(255), nullable=False)
    last_logged = Column(DateTime, default=datetime.datetime.utcnow)

    def verify_password(self, password):
22
23
24
25
26
        return blogger_pwd_context.verify(password, self.password)

    def set_password(self, password):
        password_hash = blogger_pwd_context.encrypt(password)
        self.password = password_hash

The last step is to alter our views/default.py to set the password, as shown by the emphasized lines.

40
41
42
43
        new_user.set_password(form.password.data.encode('utf8'))
        request.dbsession.add(new_user)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form}

Now our passwords are properly hashed and can be securely stored.

If you tried to log in with admin/admin credentials, you may notice that the application threw an exception ValueError: hash could not be identified because our old clear text passwords are not identified. So we should allow our application to migrate to secure hashes (usually strong sha512_crypt if we are using the quick start class).

We can easly fix this by altering our verify_password method in models/user.py.

21
22
23
24
25
26
    def verify_password(self, password):
        # is it cleartext?
        if password == self.password:
            self.set_password(password)

        return blogger_pwd_context.verify(password, self.password)

Keep in mind that for proper migration of valid hash schemes, passlib provides a mechanism you can use to quickly upgrade from one scheme to another.

Current state of our application

For convenience here are the files you edited in their entirety, with edited lines emphasized. Files already rendered in their entirety are omitted.

forms.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from wtforms import Form, StringField, TextAreaField, validators
from wtforms import IntegerField, PasswordField
from wtforms.widgets import HiddenInput

strip_filter = lambda x: x.strip() if x else None

class BlogCreateForm(Form):
    title = StringField('Title', [validators.Length(min=1, max=255)],
                        filters=[strip_filter])
    body = TextAreaField('Contents', [validators.Length(min=1)],
                         filters=[strip_filter])

class BlogUpdateForm(BlogCreateForm):
    id = IntegerField(widget=HiddenInput())


class RegistrationForm(Form):
    username = StringField('Username', [validators.Length(min=1, max=255)],
                           filters=[strip_filter])
    password = PasswordField('Password', [validators.Length(min=3)])
__init__.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    authentication_policy = AuthTktAuthenticationPolicy('somesecret')
    authorization_policy = ACLAuthorizationPolicy()
    with Configurator(settings=settings,
                      authentication_policy=authentication_policy,
                      authorization_policy=authorization_policy) as config:
        config.include('.models')
        config.include('pyramid_jinja2')
        config.include('.routes')
        config.scan()
    return config.make_wsgi_app()
templates/index.jinja2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{% extends "layout.jinja2" %}

{% block content %}

    {% if request.authenticated_userid %}
        Welcome <strong>{{request.authenticated_userid}}</strong> ::
        <a href="{{request.route_url('auth',action='out')}}">Sign Out</a>
    {% else %}
        <form action="{{request.route_url('auth',action='in')}}" method="post" class="form-inline">
            <div class="form-group">
                <input type="text" name="username" class="form-control" placeholder="Username">
            </div>
            <div class="form-group">
                <input type="password" name="password" class="form-control" placeholder="Password">
            </div>
            <div class="form-group">
                <input type="submit" value="Sign in" class="btn btn-default">
            </div>
        </form>
        <a href="{{request.route_url('register')}}">Register here</a>
    {% endif %}

    {% if paginator.items %}

        <h2>Blog entries</h2>

        <ul>
            {% for entry in paginator.items %}
                <li>
                    <a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">
                        {{ entry.title }}
                    </a>
                </li>
            {% endfor %}
        </ul>

        {{ paginator.pager() |safe }}

    {% else %}

        <p>No blog entries found.</p>

    {% endif %}

    <p><a href="{{ request.route_url('blog_action',action='create') }}">
        Create a new blog entry</a></p>

{% endblock %}
views/default.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from ..services.user import UserService
from ..services.blog_record import BlogRecordService
from ..forms import RegistrationForm
from ..models.user import User


@view_config(route_name='home',
             renderer='pyramid_blogr:templates/index.jinja2')
def index_page(request):
    page = int(request.params.get('page', 1))
    paginator = BlogRecordService.get_paginator(request, page)
    return {'paginator': paginator}


@view_config(route_name='auth', match_param='action=in', renderer='string',
             request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    username = request.POST.get('username')
    if username:
        user = UserService.by_name(username, request=request)
        if user and user.verify_password(request.POST.get('password')):
            headers = remember(request, user.name)
        else:
            headers = forget(request)
    else:
        headers = forget(request)
    return HTTPFound(location=request.route_url('home'), headers=headers)


@view_config(route_name='register',
             renderer='pyramid_blogr:templates/register.jinja2')
def register(request):
    form = RegistrationForm(request.POST)
    if request.method == 'POST' and form.validate():
        new_user = User(name=form.username.data)
        new_user.set_password(form.password.data.encode('utf8'))
        request.dbsession.add(new_user)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form}
models/user.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import datetime #<- will be used to set default dates on models
from pyramid_blogr.models.meta import Base  #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
    Column,
    Integer,
    Unicode,     #<- will provide Unicode field
    UnicodeText, #<- will provide Unicode text field
    DateTime,    #<- time abstraction field
)

from passlib.apps import custom_app_context as blogger_pwd_context


class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Unicode(255), unique=True, nullable=False)
    password = Column(Unicode(255), nullable=False)
    last_logged = Column(DateTime, default=datetime.datetime.utcnow)

    def verify_password(self, password):
        # is it cleartext?
        if password == self.password:
            self.set_password(password)

        return blogger_pwd_context.verify(password, self.password)

    def set_password(self, password):
        password_hash = blogger_pwd_context.encrypt(password)
        self.password = password_hash

Next: Summary.

Summary

Congratulations! You should now have a working blog application. But there are more possibilities.

New features

Our blog is missing a standard set of features that most web applications have. The following is a list of features and suggested Pyramid add-ons that might help with the implementation of the features.

Feature documentation

You may choose to read more details of the features that you have implemented in your blog in the Pyramid narrative documentation. Here’s a list of features with links to the relevant documentation.

The complete source code for this application is available at:

https://github.com/Pylons/pyramid_blogr

Indices and tables