05 - Controllers

Inside the logic directory _/, there's a file named controllers.py. If your site contains only static pages, then you may not need to touch this file at all. To add some logic to a page, or dynamically manipulate a template, a controller method can be added with a name corresponding to the URI. This logic will be processed before the page is rendered.

Controller Method Names

The method name should match the URI but using underscores in place of dashes and slashes. So if your URI is /products/riding-lawnmowers, ZenTools looks for a method in controllers.py like this: products_riding_lawnmowers() and a template in www/ like this: products/riding-lawnmowers.html. Remember these naming conventions and everything should work smoothly.

Index Pages

As with natural HTML, the index part of the URI path and controller method is optional. So for a file at www/products/index.html, you can access that with the URI /products/index OR simply /products. Similarly, the controller method could be named products_index() or just products(). Either is fine.

Controllers Without Templates (use stubs)

ZenTools is template-centric. The dispatcher first tries to match a request to a template and only after a template is found will it go on to look for a matching controller. So what if you need controller without a template -- for example, maybe you need a controller to just redirect to another page? The simple solution is: Create a stub template. Since template Page Settings allow comments, you can just create a file named for example, redirect-me.html like this:

# Stub for controller

The comment isn't really necessary but it's good to have a reminder just so we remember what this file is.

Passing a Single Argument to the Controller using expect()

What if your riding-lawnmowers page should dynamically display data for a particular riding lawnmower? One very clean way to construct this URI would be like this: /products/riding-lawnmowers/12 where 12 is the product id. Here's how to get that id into your controller method:

def products_riding_lawnmowers():
    product_id = expect(1)

The expect() function returns a specified number of arguments which are expected to be appended to the URI. Since no method called products_riding_lawnmowers_12() was found, the dispatcher drops the 12 and looks for the same method without the _12 in the name. Any remaining URI parts are returned by expect().

The expect() function is named as such because it is expecting to find the arguments. And if it doesn't, it will automatically raise a 404 error. This is very useful for pages which require an argument be passed to it. For example, /product-info/x where x is the product id. This page probably isn't very useful without without an id. So we use expect(1) to not only return the appended product id to our controller, but also to automatically render a 404 if no product id was given.

Another common way to use expect() is to make sure no junk is appended to the URI. If the page is /about-us and a request comes in for /about-us/monkeys/anatomy, then as long as there's a controller method for this page which can potentially process those appended arguments, the dispatcher will naturally map this request to /about-us. If you want to prevent extra junk from being appended, you can simply expect(0)

expect(0)

Passing Multiple Arguments to the Controller using expect()

The expect() function can also accept a range of arguments. Perhaps the most useful application of this functionality is the ability to start the range with 0. This effectively makes expected arguments optional.

page = expect(0, 1) # Expect zero OR one argument

In this example, we have a paginated list of results and want to allow for an optional page number to be appended to the URI. Using Python's handy or operator, we can supply the default page number 1 with a very tidy and concise syntax:

# Get appended page number or default to 1
page = expect(0, 1) or 1

Redirecting Pages: redirect_to()

Not much to it really. Just do this:

redirect_to('/some/page')

For example if you want to check the session to make sure this user is logged in, you might do something like this:

if not session.logged_in:
    redirect_to('/login')

We'll learn more about sessions in a later section.

Revealing Data to the HTML Document

Often during the development process, it can be very useful to just quickly and easily find out the value of a variable, or a dictionary, or even an object. In these cases, we simply use html.reveal() as in this example:

order = models.Order.get(expect(1))
html.reveal(order)

This will reveal the order object in the content of the html document. You may also pass multiple arguments to reveal() like this:

html.reveal(order, customer, session)

It's also possible to reveal to the log. Please see the log section for more on that.

raise Error404 and raise Error500

Sometimes you just want to raise an error. Of course there's nothing to stop you from printing your error messages on screen. But for typical cases warranting a real 404 or 500 error (resource not found or application error), you can raise these errors yourself. ZenTools will use the _404.html or _500.html template as appropriate. HTTP headers are also taken care of correctly.

if not find_something():
    raise Error404

if not process_order():
    raise Error500

Giving user feedback with flash()

This feature was "borrowed" directly from Ruby on Rails. We liked it so much that we pretty much just stole it outright and didn't even bother to rename it. The idea is, it's a very common pattern in web applications to process a form submission, then redirect to another page which should display for example a success message. Here's how we do that using the flash() function.

if product.save():
    flash('Product saved.') and redirect_to('/admin/products')

This saves the string 'Product saved.' into the "flash" session for the next request only. And as long as there is a <div id="flash-message"></div> somewhere on the next page, it will be populated with that string. The flash message is cleared from the session automatically after each subsequent request.

Also, if there is no flash message in the session, then the <div id="flash-message"></div> element is deleted from the template. This means you can style your flash message as a bright red box if you like. And it will only be present if a flash-message has been set in the previous request.

Giving user feedback with flash_now()

Sometimes you want to give user feedback without redirecting to a new page. Since the normal flash() function only sets the message to be displayed on the next request, this won't work for some situations. For example, if a form is submitted with invalid entries, we might want to display a flash-message saying something like, "Please double check your form entries." -- but without redirecting because we want to display the populated form on this page. So for situations where we want to set the flash-message to be displayed right now rather than on the next request, we can use the flash_now() function.

if product.save():
    flash('Product saved.') and redirect_to('/admin/products')
else:
    flash_now('Unable to save product.')

Getting post data

All values submitted by POST are available as part of the post object. The standard ZenTools way to submit forms is to set the form's action attribute to an empty string like this:

<form id="my-form" method="post" action="">

This means the form will submit to itself, the same URI as the form. That way we can keep all our logic for form processing in the same controller, and redirect only after a successful form submission.

So a typical controller for receiving posted data might look like something like this:

if post:
    product = models.Product(post)
    if product.save():
        flash('Product saved.') and redirect_to('/admin/products')

The post object will evaluate to None if nothing was posted. Each submitted name/value pair is accessible as an attribute of post. For example, a simple email form might contain the named fields name, email, and message. Those submitted values would be available in the controller method as post.name, post.email, and post.message.

Updating an object with post data

A typical form for editing an existing record requires first getting the record from the database, then modifying that object with values submitted through the web form.

Of course you could do it manually, overwriting each attribute with the one submitted in post. But a far more efficient way exists as long as all the names match-up. Here's an example:

def edit_product():
    product = models.Product.get()
    if post:
        product.update_with(post)
        product.save()

Without worrying too much yet about getting and saving Model objects, it's easy to see what the update_with() method does. In this example, for any attribute of post with a name matching one in product, the value is changed.

It should be noted that any values not matching are silently ignored. So if the form contained an element named stupidity_tax, but the product object contained no such attribute, the update_with() would simply skip over that attribute and continue to update the others. Similarly, if we try to update an object with some attributes missing, that's also no problem. The update_with() method will simply update whatever it can.

Updating with unchecked checkboxes

One potential trouble-spot is using update_with() with submitted checkbox values. According the W3C standard for HTTP, checkboxes are not included at all in HTTP POST unless they are checked. This means that we have no way of determining whether the checkbox was unchecked or was simply not included as an option on the form. So if you want unchecked checkboxes to be treated as False when applying update_with to an object, you must initialize those values first like this:

def edit_product():
    product = models.Product.get()
    if post:
        product.sold_out = False
        product.active = False
        product.update_with(post)
        product.save()

Next: 06 - Template Formating