7.1. Dashboard Controller
If you keep adding more and more methods to the same controllers.py file, it's going to get out of hand pretty quickly. Luckily, it's easy to handle this growing complexity by moving some pieces out to separate modules. WhatWhat has separate controller modules for the Dashboard, Project, People, Recent Changes, and Feed features. We won't have time to look at each of these in depth, but we do take a look at a few of them to see what we can learn.
Remember that everything in CherryPy is built up off of the root controller. To mount classes from other modules, you just import the controller you're keeping in a separate file and mount it within your root controller. Here's how WhatWhat does it:
Note that the method in the root controller is using raise tg.redirect('/dashboard') to take anybody who comes to a WhatWhat website and send them directly on to /dashboard. And, this generates a call to the dashboard class, which is an alias for DashboardControler. Then, because there is no specific method name called within DashboardController, the index method is triggered.
If we take a look into dashboard.py in the subcontrollers subdirectory, we can see what happens when the index method is called:
(There's also a new_project method, but we come back to that later.)
Whatwhat dashboard.py import turbogears as tg from turbogears import identity, validators from fasttrack.model import Person, Project, status_codes class DashboardController(identity.SecureResource): require = identity.not_anonymous() @tg.expose(template="fasttrack.templates.dashboard.index") def index(self): all_projects = Project.select("parent_project_id is Null" + "order by upper(name)") projects = [project for project in all_projects if not project.archived] archived_projects = [project for project in all_projects if project.archived] people = Person.select(orderBy='displayName') return dict(active_section='dashboard', projects=projects, archived_projects=archived_projects, people=people, status_codes=status_codes)
7.1.1. WhatWhat Security and Identity
Before we start looking inside of the index method, there's another import TurboGears feature at use here that deserves a closer look. Jeff Watkins created an authentication and authorization framework that allows TurboGears applications to define users, groups, and permissions and makes it easy to secure particular resources in your website to specific users or those who are members of a particular group. Jeff Watkins created TurboGears identity with a remarkably simple application programming interface (API).
You can decorate any controller with the require method from the turbogears.identity, with whatever restrictions you need. To restrict access to the DashboardController index method to only logged-in users, you could just write something like this:
@tg.expose(template="whatwhat.templates.dashboard.index") @require(identity.not_anonymous) def index(self): pass
The require decorator checks to see whether the current request is coming from a logged-in user, and redirects the user to the login page if necessary. Of course, if all the identity module did was give you the ability to require that a user be logged in, it wouldn't be all that helpful. The TurboGears identity module has a simple but powerful developer interface. You can restrict access to pages to particular users or groups, you can set up specific permissions for your page, and you can associate that permission with groups. And you can check things such as in_any_group, in_all_group, or has_all_permissions. You can also access the identity information from within your controller, allowing you to write code like this:
from turbogears import identity if "editor" in identity.current.groups: value = "Do something interesting here" else: value = "Do something less interesting here"
If you pass the identity objects into your template (or import them using a Python block <? from turbogears import identity ?>), you can easily use check particular permissions with py:if to hide specific page sections or administrative links from regular users.
Although the decorator syntax makes TurboGears identity easy to use, it's also a bit cumbersome when you want to restrict access to an entire directory tree. But TurboGears makes that easy, too.
The identity module provides a class for you to subclass whenever you want to protect a whole swath of your site all at once. The class that does this for you is called SecureResources. All you have to do is create a controller class, which inherits from SecureResources, and override the require method with whatever restrictions you need.
This is exactly how WhatWhat uses the identity module here. Because WhatWhat restricts access to all the dashboard methods to logged-in users, the DashboardController class subclasses identity.SecureResource and assigns require to identity.not_anonymous.
Therefore, all the methods of the DashboardController class will only be accessible to logged-in users. Behind the scenes, the SecureResource class contains code that prevents any user who browses to any of the protected methods in the DashboardController subclass without logging in first from accessing any of the subpages of /dashboard.
Any class that subclasses SecureResources automatically redirects users who fail to meet the required criteria to either a login page, with a flag that lets the login page know where to send the user when the login is complete, or to a page that tells users that their user account does not have sufficient permission to view that resource. Of course, if the would-be user does not have an account, and can't authenticate, that user won't be able to get in at all.
If this seems a bit simplistic to you, it is; but it's all that WhatWhat requires. As you've briefly seen, the identity framework does allow for much more complex authorization logic, but WhatWhat is designed to help project teams and company managers communicate their experiences more widely. So, it isn't designed to keep data secret from any of the managers or project members.
If you have different needs, however, it is simple to modify it and thus create a much more complex permissions structure. If you create users and add them to the admin group (using CatWalk or tg-admin shell), you could replace identity.not_anonymous with identity.in_group('admin').
And if you have several groups that ought to have access to the Dashboard view, you can create a view_dashboard and grant the admin, manager, project_managers, and team_leads group the view_dashboard permission. By default, identity stores these permissions in your database using a many-to-many relationship between groups and permissionsjust like there was between bookmarks and categories. So, adding new permissions uses the same syntax: TG_Group.addTG_Permission("view-dashboard"). Instead of covering all of that right now, however, we devote much of Chapter 22, "TurboGears Identity and Security," to covering the TurboGears identity framework.
7.1.2. Exploring the Dashboard Index
After the user has been authenticated, the main body of the index method is evaluated:
@tg.expose(template="whatwhat.templates.dashboard.index") def index(self): all_projects = Project.select("parent_project_id is Null" + "order by upper(name)" projects = [project for project in all_projects if not project.archived] archived_projects = [project for project in all_projects if project.archived] people = Person.select(orderBy='displayName') return dict(active_section='dashboard', projects=projects, archived_projects=archived_projects, people=people, status_codes=status_codes)
DashboardController's index method is concerned with getting each of the top-level projects from the Projects table and sorting them into archived and current projects.
Probably the biggest new thing we are seeing here is the Project.select (parent_projectID…) method call. If you remember our model had a one-to-many relationship between projects and subprojects. In effect, this creates a column with the ID value of the parent project of each of our projects. Only the projects with a Null value in this column are top-level projects, and for now we want just the top-level projects.
Of course, Project.select() itself isn't new if you've been following along. We first used the select method by passing it no arguments at all, as in Project.select(), in which case we got back the contents of every column for every single row in the database, just as if we had written a SQL query such as this:
SELECT * FROM person
Then we took a quick look at how you can pass a Python expression such as the following:
person.select(firstName = "Carl")
This statement returns a set of all the objects with a first name value of firstName== "Carl"just as if we had written a SQL query such as this:
SELECT * from Person where first_name="Carl".
When you use an SQLObject's select method, SQLObject determines what SQL to send to the database for you, which is nice. But even more important, it helps you avoid SQL injection attacks by automatically escaping everything. If you use your own string stuff to build a where clause, you can easily get hit by a SQL injection attack if you're not careful.
SQL injection attacks are created when users "inject" some SQL commands into the form they are filling out. If you then use that string in a SQL query (like an insert that is intended to put the contents of that file into the database), you can end up running the code that the users wrote directly against your database.
You can also create much more complex Select queries using SQLObject's querybuilder syntax, which you learn more about in Chapter 12, "Customizing SQLObject Behavior." That's generally the way we recommend creating queries in TurboGears.
But, because Python and SQLObject both have an attitude that says "We're all consenting adults here," there is nothing to stop you from writing your where clauses by hand. We respect your right to evaluate the options and make rational choices about what you do in the privacy of your own controller module (even if that means doing something some programmers think is wrong).
And that's just what WhatWhat is doing.
all_projects = Project.select("parent_project_id is Null order by upper(name)")
But as long as you're careful, there's nothing wrong with writing where clauses by hand like this. And in this example from WhatWhat, there is absolutely no chance of an SQL injection attack because the code just passes a plain string with no string substitution at all.
If you look closely at the preceding code, you'll notice one other gotcha for writing your SQL by hand this way. The WhatWhat Project class defines
parent_project = ForeignKey('Project')
Notice that this select query looks for parent_project_id rather than parent_project. This is because SQLObject automatically creates column names for you based on some internal rules. Of course, you can modify these rules, and we tell you how in Chapter 12. But for now, all you need to know is that whenever you create a query by hand in SQLObject, you have to use the actual name of the column in the table. You can do this by either memorizing the SQLObject rules for creating table names or looking them up in your database whenever you want to use them.
After index has an SQLObject result set with all the top-level projects in it, it uses two list comprehensions to split that into a list of current projects and a list of archived projects. It also pulls down a SQLObject SelectResults object for all the people in the database.
This is another good place to think about what belongs in the controller and what belongs in the model. In all of WhatWhat, the dashboard controller is the only place that needs a list of top-level projects sorted into archived and current projects. So, it is probably fine to leave this logic here. But if ever there are other places where this same data is required somewhere else, it makes a lot of sense to move this into the model. The model-viewer-controller (MVC) puritans among us might even say that this kind of thing should always go in the model; but because this often grows up organically in the controller over time, it might not be worth moving it to the model until you find yourself needing that same data somewhere else. The key principle is to avoid repeating the same code in multiple places.
So, after everything is said and done, the index method passes a dictionary to our Kid template (whatwhat.templates.dashboard.index). In that dictionary, we have an active_session marker, a list of current projects (called projects), a list of archived_projects, a "list" of people, and a list of possible status codes (which we imported from our model).