Python Web Framework Series – Pylons: Part 3 Views with Mako

This is a huge post and I should have split this into several smaller ones so please bear with me while I get my series format tweaked.  We last left off with Controllers, Views and Testing with a basic test, basic view and basic controller.  Now with our basic scaffold built we can focus on making our views reusable, dynamic, and more pleasant to look at.

First things first

First before we dive too deeply into Mako, lets setup a default home page by doing the following:

  1. delete the index.html in the pylonsforumpublic folder.
  2. in pylonsforumconfigrouting.py add the following line somewhere under the line “# CUSTOM ROUTES HERE” but before “return map”.
    1. map.connect(“home”, “/”, controller=”home”, action=”index”)
  3. assuming you have your Pylons command we used earlier  paster serve –-reload development.ini then open your browser to http://127.0.0.1:5000 and you should see the view we created in the last lesson.

Basics

 

Mako uses standard python mixed with HTML to display views. This gives you both a great deal of flexibility and a great deal of ability to hang yourself. So be warned, avoid putting to much logic into your view, since you can do about anything there.

Open up templatesindex.mako replace the table rows in the table body with the following:

<tbody>
%for p in c.posts:
<tr><td>${p.author}</td><td>${p.subject}</td><td>${p.dateadded}</td></tr>
%endfor 
</tbody>

for good measure in the beginning of the body before the “recent posts” div place the following

<body>
username: ${c.username}                
<div id=“recentposts” style=“float: right”>

Looking at this, for the tables we’ve done a for loop over some variable called c.posts. In the second we’re accessing another variable c.username. “c” is shorthand for Template Context, similar to ViewData in Asp.Net MVC and PropertyBag in Monorail, except we’re not accessing a string dictionary. 

Add the following tests in “pylonsforumtestsfunctionaltest_home.py”  for what we need to add to the controller:

    def test_username(self):
        response = self.app.get(url(controller=‘home’, action=‘index’))
        assert “username: rsvihla” in response

    def test_recent_posts(self):
        response = self.app.get(url(controller=‘home’, action=‘index’))
        assert “<tr><td>jkruse</td><td>New Kindle</td><td>06/24/2009</td></tr>” in response

running nosetests –-with-pylons=test.init should give you two assertion failures.

now change “pylonsforumcontrollershome.py” to look like so:

import logging

from pylons import request, response, session, tmpl_context as c
from pylons.controllers.util import abort, redirect_to

from pylonsforum.lib.base import BaseController, render

log = logging.getLogger(__name__)

class Post(object):

    def __init__(self, author, subject, dateadded):
        self.author = author
        self.subject = subject
        self.dateadded = dateadded

class HomeController(BaseController):

    def index(self):
        c.username = “rsvihla”
        c.posts = [Post("jkruse", "New Kindle", "06/24/2009")]
        return render(‘index.mako’)

We’ve added a Post class with basic attributes and placed them in an array in the c.posts variable, also we’ve hardcoded the username “rsvihla”.  I know the more experience developers here are cringing at my awful little Post class, don’t worry its a just a place holder and will be removed later.  The point here is building functionality in steps with test coverage.  Now run nosetests just as before and you should have all tests passing.  For bonus measure refresh http://127.0.0.1:5000 .

Base Layouts

We’ve built a very basic page now, but suppose we want to build several and have some bits of information show up over and over again, like user name or menu structure.

create a base template named “base.mako” in the templates directory that has the following in it:

<%namespace name=“user” module=“pylonsforum.model.users” />
<!DOCTYPE html PUBLIC ”-//W3C//DTD XHTML 1.0 Transitional//EN” ”http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html>
<head>
<title>${self.title()}</title>
${self.head_tags()}
<style type=“text/css”>
#header{ 
height: 70px;
width: 100%;
border-width: 0.5px;
border-style: solid;
font-size:30px;
text-align: center;
background-color: #9B2C25;
}
#sidebar{
background-color: #C1AD72;
border-width: 0.5px;
border-style: dotted;
width: 150px;
height: 600px;
float: right;
font-size: 15px;
padding-top:10px;
padding-left:5px;
}
#content{
margin-top:10px;
}
body {
background-color: #DACEAB;
}
</style>
</head>
<body>

<div id=“header”>Pylons Forum</div>
<div id=“sidebar”>
username: ${user.get_current_user()}
</div>

<div id=“content” >
${self.body()}
</div>

</div>
</body>
</html>

Most everything is standard HTML so lets restrict this to the interesting bits:

<%namespace name=“user” module=“pylonsforum.model.users” />

The namespace directive here is basically doing an import of a custom module and giving it an alias of user. This is no different than in normal python code typing:

import pylonsforum.model.users as user

 

This is used later in the div sidebar by pulling the current user from my customer users module:

username : ${user.get_current_user()}

the module source only contains a simple hard coded value for now so in pylonsforummodel make a users.py file with only the following:

def get_current_user(self):
    return “rsvihla”

 


 

Now onto the next non-HTML tidbits

 

<title>${self.title()}</title>
${self.head_tags()}
${self.body()}

 

Everyone of the above methods establishes a base method that the child templates must now call with the exception of self.body (which is called when the templates actually render the content anyway).  So lets adjust “index.mako” template to the following:

<%inherit file=“/base.mako”/>
<%def name=“title()”>Pylons Forum</%def>
<%def name=“head_tags()”></%def>
<div id=“recentposts”>
<table>
<thead><tr><th>subject</th><th>author</th><th>date submitted</th></tr></thead>
<tbody>
%for p in c.posts:
    <tr><td>${p.author}</td><td>${p.subject}</td><td>${p.dateadded}</td></tr>
%endfor 
</tbody>
</table>
</div>

The inherit line at the very top references our base.mako template. The next two lines with the <%def tag are where our previous self.head_tags() and self.title() methods are referenced.

You’ll notice the title method is passing in a new title for the page, and our head_tags method is doing nothing, so no changes there. Browse to your pylons home page and you should see something resembling this:

image

Your tests should also still pass with no modification.

image

Forms and Web Helpers

Lets add a basic submit form and a link in the base.mako template to go to the new form page.

Add the following tests to test_home.py

    def test_new_thread(self):
        response = self.app.get(url(controller=“home”, action=“newthread”))
        assert “<input id=subject name=subject type=text />” in response
    
    def test_submit_new_thread(self):
        response = self.app.post(url(controller=“home”, action=“submitnewthread”), params={‘subject’:‘test’,‘content’:‘testcontent’})
        assert “post submitted” in response

Install FormBuild with the following command:

image

Next open libhelpers.py and change the imports to as follows:

image

In the sidebar div under username add

<br/>
${h.link_to(‘new thread’, h.url_for(controller=‘home’, action=‘newthread’))}

Now add an action on the home controller for :”new thread” and create a view in your templates folder called newthread.mako.

Add the following to your home.py controller:

 

    def newthread(self):
        return render(‘newthread.mako’)

 

The newthread.mako file should have the following in it:

<%inherit file=“/base.mako”/>
<%def name=“title()”>Make A New Thread</%def>
<%def name=“head_tags()”></%def>
${h.form_start(h.url_for(controller=‘home’, action=‘submitnewthread’), method=“post”)}
    ${h.field(“Subject”, field=h.text(name=‘subject’))}
    ${h.field(“Content” ,field=h.textarea(name=‘content’))} 
    ${h.field(field=h.submit(value=“Create Thread”, name=‘submit’))}
${h.form_end()}

Sooooooo what are h values everywhere?  Do I need them? They come from our earlier imports in libhelpers.py , and no you do not actually need to use them, a typical HTML form tag is all you actually need, this is an available option for those interested.

  • h.form_start and h.form_end are pretty self explanatory
  • h.field takes label text as argument one, and then a url for argument two.
  • h.text, h.textarea, h.hidden and h.submit represent their html counterparts.
  • h.url_for takes a controller and action to make a url for you.

 


We now need another action for actually posting our data. so add another method to our home.py controller:

     def submitnewthread(self):
        c.username = users.get_current_user(self)
        c.subject = request.POST['subject']
        c.content = request.POST['content']
        return render(‘submitnewthread.mako’)

With the following import at the top

import pylonsforum.model.users

 

Our submitnewthread.mako view should have the following:

<%inherit file=“/base.mako”/>
<%def name=“title()”>Thank You For Your Submission</%def>
<%def name=“head_tags()”></%def>
<p>post submitted </p>
data was as follows <br/>
author:  ${c.username} <br/>
subject: ${c.subject} <br />
content: ${c.content} <br />

newthread view should look like so

image

and submitnewthread page should look like this

image

Validation

 

Validation is a huge subject in Pylons, I’m going to just focus on the most basic common case.

Open home.py controller again and add these two imports to the top

import formencode
from pylons.decorators import validate

 

Then in the same file for now below the post object add

 

class PostForm(formencode.Schema):
    allow_extra_fields = True
    filter_extra_fields = True
    subject = formencode.validators.String(not_empty=True)
    content = formencode.validators.String(not_empty=True)

 

Finally on top of the submitnewthead action in the same class file add

    @validate(schema=PostForm(), form=‘newthread’, post_only=True, on_get=True)
    def submitnewthread(self):

Now if you attempt to leave either the subject or content fields blank PylonsForum will now notify you of what you missed.

image

 

When done your tests should all pass

image

Summary

We went through the 80% cases for views, controllers and a brief bit about testing the response object. We still haven’t done a lot of unit testing in the traditional sense but we’ve been focusing exclusively on UI.  Stay tuned next for my SQL Alchemy piece.

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Ryan Svihla

I consider myself a full stack polyglot, and I have been writing a lot of JS and Ruby as of late. Currently, I'm a solutions architect at DataStax
This entry was posted in Mako, Pylons, Python. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://govindkanshi.wordpress.com Govind

    First things first – should point to “available” controller – we do not have login controller yet

    map.connect(“home”, “/”, controller=”login”, action=”index”) should be
    map.connect(“home”, “/”, controller=”home”, action=”index”)

  • Ryan

    I am little confused as tests always complain there is no class user. Although you mention module users(which I think should be model user).

    def get_current_user(self):
    return “rsvihla”

    Which file does this code go to?

  • http://www.lostechies.com/members/rssvihla/default.aspx Ryan Svihla

    @Govind nice catch and has been corrected now.

    @Ryan in pylonsforum\model\ make a file named users.py and place that line in it. Updated the article text with a better explanation.

    Thanks again

  • Govind

    Wow ! that was pretty quick.
    There is similarly small nitpick with paster server – it should be paster serve.

    Thanks again for posting such a nice tutorial.

  • http://www.buresund.se/ Roland Buresund

    in submitnewthread the line:
    c.username = uses.get_current_user(self)
    doesn’t parse, as uses doesn’t exist (should probably be user).

    Also, the line
    import pylonsforum.model.users
    that is supposed to be added to home.py, doesn’t fit into this scenario, as the intention probably was to write (based on text in a previous paragraph):
    import pylonsforum.model.users as user

    Nitpicking, I know, but I like the tutorial and would like it to be a success :-)

  • http://www.lostechies.com/members/rssvihla/default.aspx Ryan Svihla

    @Roland

    thanks for the typo catch.

    The other parts you’re commenting come into play later, and are only used as a place holder.

    Ultimately however whether something is in a controller explicitly or not is a style choice.