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.
Documentation links¶
The complete source code for this application is available on GitHub at:
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 installvirtualenv
. - 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¶
- Install Python 3.3 or greater
- Workspace and project directory structure
- Set an environment variable
- Create a virtual environment
- 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 © 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 argumentlocation
that will add aLocation:
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 {}
|
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.
- Delete a blog entry
- Testing
- Forgot password, which sends an email to the user to reset their password, using events
- HTTP caching
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: