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.