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 {}
|