Warning: Can't synchronize with repository "(default)" (Unsupported version control system "svn": No module named svn). Look in the Trac log for more information.

Changes between Initial Version and Version 1 of IntroductionToWidgets


Ignore:
Timestamp:
04/22/06 18:36:34 (13 years ago)
Author:
tlesher@…
Comment:

Added.

Legend:

Unmodified
Added
Removed
Modified
  • IntroductionToWidgets

    v1 v1  
     1= Introduction =  
     2 
     3Widgets are one of the most useful features added to TurboGears 0.9. Unfortunately, they're also one of the least-documented. This tutorial will explain why widgets are so useful, and show you how to create your own lightweight widgets, with a bonus introduction to the standard !DataGrid widget. 
     4 
     5= The Fooball Application = 
     6 
     7We're going to create a web application that tracks statistics for the entirely fictitious sport of "Fooball".  Fooball is a very simple game: players on teams do unspecified things to score as many points as possible before the end of the game. The team with the most points at the end wins. The player with the most points at the end gets lucrative sponsorship deals, but that's outside the scope of this tutorial. 
     8 
     9The front page will contain a list of teams and a list of all league players and their stats.  The list of teams will contain links to individual team pages, each listing stats for the players on that team.  Simple enough. 
     10 
     11I'll assume that you know enough TurboGears to "quickstart" the application; if not, see the TurboGears documentation. I'm going to quickstart the application using 'fooball' for both the application and module name, and I'll assume that you've properly configured the database connection in your "dev.cfg" file (I'm using an SQLite backend on Windows XP). 
     12 
     13Now, let's define our data model in {{{model.py}}}: 
     14 
     15{{{ 
     16#!python 
     17import datetime 
     18 
     19class Player(SQLObject): 
     20   name = StringCol(length=40, alternateID=True) 
     21   birthdate = DateCol(notNull=True) 
     22 
     23   team = ForeignKey('Team') 
     24 
     25   points = IntCol(default=0) 
     26 
     27class Team(SQLObject): 
     28   city = StringCol(length=20, notNull=True) 
     29   nickname = StringCol(length=20, notNull=True, alternateID=True) 
     30    
     31   players = MultipleJoin('Player') 
     32}}} 
     33 
     34From the model, you can see that teams are uniquely identified by their nickname (although a city can host multiple teams), and all players forever belong to one team (Fooball players haven't discovered free agency yet, to the delight of Fooball team owners). 
     35 
     36We'll create the database using "tg-admin sql create", and define some data using "tg-admin shell". You could use !ModelDesigner and Catwalk to do this, but for simplicity we'll use the command line tools. 
     37 
     38{{{ 
     39#!python 
     40>>> import datetime 
     41>>> t1 = Team(city='Pittsburgh', nickname='Ferrous Metals') 
     42>>> t2 = Team(city='Seattle', nickname='Seagulls') 
     43>>> Player(name='Bob Waffleburger', birthdate=datetime.date(1982,3,2), team=t1, points=21) 
     44<Player 1 name='Bob Waffleburger' birthdate='datetime.date(198...)' teamID=1 points=21> 
     45>>> Player(name='Mike Handleback', birthdate=datetime.date(1975,9,25), team=t2, points=10) 
     46<Player 2 name='Mike Handleback' birthdate='datetime.date(197...)' teamID=2 points=10> 
     47>>> hub.commit() 
     48}}} 
     49 
     50Good enough for now. (Any resemblance to real teams, players, or final scores is purely coincidental, but I'm sure you knew that.) 
     51 
     52Now we'll cobble up a simple front page by editing the body of {{{\fooball\fooball\templates\welcome.kid}}}: 
     53 
     54{{{ 
     55<h1>International Fooball League Stats</h1> 
     56<h2>Teams</h2> 
     57<table border="1"> 
     58  <tr> 
     59    <th>City</th> 
     60    <th>Team Name</th> 
     61  </tr> 
     62  <tr py:for="team in teams"> 
     63    <td py:content="team.city"/> 
     64    <td py:content="team.nickname"/> 
     65  </tr> 
     66</table> 
     67 
     68<h2>Players</h2> 
     69<table border="1"> 
     70  <tr> 
     71    <th>Name</th> 
     72    <th>Birthdate</th> 
     73    <th>Team</th> 
     74    <th>Points</th> 
     75  </tr> 
     76  <tr py:for="player in players"> 
     77    <td py:content="player.name"/> 
     78    <td py:content="player.birthdate"/> 
     79    <td py:content="player.team"/> 
     80    <td py:content="player.points"/> 
     81  </tr> 
     82</table> 
     83}}} 
     84 
     85Since our template uses the {{{teams}}} and {{{players}}} variables, add them to the Root controller's index method in {{{controllers.py}}}: 
     86 
     87{{{ 
     88#!python 
     89from model import Team, Player 
     90 
     91class Root(controllers.RootController): 
     92    @expose(template="fooball.templates.welcome") 
     93    def index(self): 
     94        return dict(teams=Team.select(), 
     95                    players=Player.select()) 
     96}}} 
     97 
     98= The First Attempt = 
     99 
     100Now, start the app, and take a look at the front page: 
     101 
     102***screenshot*** 
     103 
     104Ugh.  Well, the teams look fine, but the players show an ugly SQLObject representation for their team name.  We could fix this by changing the {{{player.team}}} to {{{players.team.city}}}, but then we'd have to make sure we do it everywhere we want to show a {{{Player.team}}}.  And make sure we always do it the same way.  That's a recipe for mistakes. When programming, it's best to follow the maxim, "Don't Repeat Yourself". 
     105 
     106Instead, we'll tell the {{{Team}}} object how to display itself by adding a string-izing method to the class: 
     107 
     108{{{ 
     109#!python 
     110def __str__(self): 
     111    return "%s %s" % (self.city, self.nickname) 
     112}}} 
     113 
     114And look: reuse! We can get rid of that ugly "City/Name" table by doing the same thing there in the {{{welcome.kid}}} template: 
     115 
     116{{{ 
     117<h2>Teams</h2> 
     118<table border="1"> 
     119  <tr> 
     120    <th>Team</th> 
     121  </tr> 
     122  <tr py:for="team in teams"> 
     123    <td py:content="team"/> 
     124  </tr> 
     125</table> 
     126}}} 
     127 
     128***screenshot*** 
     129 
     130Much better. Now let's make pages for each team.  A new controller method will do the trick: 
     131 
     132{{{ 
     133#!python 
     134    @expose(template="players.templates.team") 
     135    def team(self, team_id, *args, **kw): 
     136        return dict(team=Team.get(int(team_id))) 
     137}}} 
     138 
     139With a new template called "team.kid", whose body looks like this: 
     140 
     141{{{ 
     142<h1 py:content="team"/> 
     143<h2>Players</h2> 
     144<table border="1"> 
     145  <tr> 
     146    <th>Name</th> 
     147    <th>Birthdate</th> 
     148    <th>Points</th> 
     149  </tr> 
     150  <tr py:for="player in team.players"> 
     151    <td py:content="player.name"/> 
     152    <td py:content="player.points"/> 
     153    <td py:content="player.birthdate"/> 
     154  </tr> 
     155</table> 
     156}}} 
     157 
     158And a way to get to the team page, courtesy of a quick change to the welcome template: 
     159 
     160{{{ 
     161<h2>Teams</h2> 
     162<table border="1"> 
     163  <tr> 
     164    <th>Team</th> 
     165  </tr> 
     166  <tr py:for="team in teams"> 
     167    <td><a href="${'/team/%d' % team.id}">${team}</a></td> 
     168  </tr> 
     169</table> 
     170}}} 
     171 
     172And... 
     173 
     174***welcome screenshot*** 
     175 
     176Ok, let's look at the team page for Pittsburgh: 
     177 
     178***team screenshot*** 
     179 
     180Whoops. Haha. I don't think "Big Bob" was born on 0, and he certainly didn't earn 1983-03-02=1975 points. When I typed the second table, I switched the order of two columns. 
     181 
     182This again illustrates the "Don't Repeat Yourself" point. Every time you write the same code again, you run the risk of introducing bugs. And if you want to change the way the table looks (say, by showing 'age' instead of 'birthday', you have to repeat the change each time. 
     183 
     184= Enter the Widget =  
     185 
     186Rather than retype (or copy and paste) that table every time we want to show a grid of players, let's create a reusable widget. We'll create the simplest possible widget that we can use in both the front page and the team page.  We'll add it to {{{controllers.py}}} for now: 
     187 
     188{{{ 
     189#!python 
     190from turbogears import widgets 
     191from model import Team, Player 
     192 
     193class SimpleRosterWidget(widgets.Widget): 
     194    template = ''' 
     195    <table xmlns:py="http://purl.org/kid/ns#" class="simpleroster" border="1"> 
     196      <tr> 
     197      <th>Name</th> 
     198        <th>Birthdate</th> 
     199        <th>Team</th> 
     200        <th>Points</th> 
     201      </tr> 
     202      <tr py:for="player in value"> 
     203        <td py:content="player.name"/> 
     204        <td py:content="player.birthdate"/> 
     205        <td py:content="player.team"/> 
     206        <td py:content="player.points"/> 
     207      </tr> 
     208    </table> 
     209''' 
     210}}} 
     211 
     212And that's it. Not a drop of code to be found. You'll note that the template references "value", which is the standard TurboGears name used to apply data to the widget. I also added a CSS class to the widget; I like to do that because it's easier to apply per-widget CSS styles down the road. 
     213 
     214Let's provide the widget in the controllers: 
     215 
     216{{{ 
     217#!python 
     218class Root(controllers.RootController): 
     219    @expose(template="fooball.templates.welcome") 
     220    def index(self): 
     221        return dict(teams=Team.select(), 
     222                    players=Player.select(), 
     223                    players_widget=SimpleRosterWidget()) 
     224 
     225    @expose(template="fooball.templates.team") 
     226    def team(self, team_id, *args, **kw): 
     227        return dict(team=Team.get(int(team_id)), 
     228                    players_widget=SimpleRosterWidget()) 
     229}}} 
     230 
     231And change the welcome template to use the widget: 
     232 
     233{{{ 
     234<h2>Players</h2> 
     235${players_widget.display(players)} 
     236}}} 
     237 
     238***screenshot*** 
     239 
     240= Template Parameters =  
     241 
     242This looks good so far. Let's do the same to the team template: 
     243 
     244{{{ 
     245<h2>Players</h2> 
     246${players_widget.display(team.players)} 
     247}}} 
     248 
     249***screenshot*** 
     250 
     251Hmm.  Well, it works, but it seems a bit silly to specify the team column for the team roster, since it will always be the same. We could create separate widgets for each page, but that defeats the reusability of widgets.  So let's add a parameter to the {{{SimpleRosterWidget}}} class to disable the team column: 
     252 
     253{{{ 
     254#!python 
     255class SimpleRosterWidget(widgets.Widget): 
     256    template_vars=['with_team'] 
     257     
     258    def __init__(self, with_team=True, *args, **kw): 
     259        super(SimpleRosterWidget,self).__init__(*args, **kw) 
     260        self.with_team=with_team 
     261         
     262    template = ''' 
     263    <table xmlns:py="http://purl.org/kid/ns#" class="simpleplayer" border="1"> 
     264      <tr> 
     265      <th>Name</th> 
     266        <th>Birthdate</th> 
     267        <th py:if="with_team">Team</th> 
     268        <th>Points</th> 
     269      </tr> 
     270      <tr py:for="player in value"> 
     271        <td py:content="player.name"/> 
     272        <td py:content="player.birthdate"/> 
     273        <td py:if="with_team" py:content="player.team"/> 
     274        <td py:content="player.points"/> 
     275      </tr> 
     276    </table> 
     277''' 
     278}}} 
     279 
     280There's a bit going on here: 
     281 
     282 * We added a class attribute called {{{template_vars}}}.  When TurboGears renders the template, it looks for this attribute. Any attribute names in this list that exist on the template instance will be added to the variables provided to the template.  So at render time, if the template has a {{{with_team}}} attribute, the template will be able to access it. 
     283  
     284 * We added an {{{__init__}}} method that calls the parent class ({{{Widget}}}, in this case), and stores the {{{with_team}}} argument (if any).  Since we don't want to worry about what the base class is, we use Python's {{{super}}} function, and since we don't want to worry about what arguments might be there, we use {{{*args}}} and {{{**kw}}} to pass along any extra positional or keyword arguments to the base class. 
     285  
     286 * Since we've added "with_team" to the template_vars, we can use its value to determine whether or not to display the team name: 
     287 
     288{{{ 
     289#!python 
     290    @expose(template="fooball.templates.team") 
     291    def team(self, team_id, *args, **kw): 
     292        return dict(team=Team.get(int(team_id)), 
     293                    players_widget=SimpleRosterWidget(with_team=False)) 
     294}}} 
     295 
     296***screenshot*** 
     297 
     298Great.  Now we're done.  
     299 
     300But, as Mr. Jobs is so fond of saying, there's "one more thing..." 
     301 
     302= Now, Don't Do That! = 
     303 
     304Now that you've seen how to create your own table-based, customizable widget, I'm going to tell you not to do that. Creating your own widgets is a fast and easy way to create reusable and customizable user interfaces, but if you're just doing a simple table layout like our roster grid, the TurboGears widgets library already gives you a great, pre-made widget: {{{DataGrid}}}. 
     305 
     306To use {{{DataGrid}}}, just change the {{{index}}} and {{{team}}} controller methods: 
     307 
     308{{{ 
     309#!python 
     310class Root(controllers.RootController): 
     311    @expose(template="fooball.templates.welcome") 
     312    def index(self): 
     313        player_fields = [('Name', 'name'), 
     314                         ('Birth Date', 'birthdate'), 
     315                         ('Team', 'team'), 
     316                         ('Points', 'points')] 
     317        return dict(teams=Team.select(), 
     318                    players=Player.select(), 
     319                    players_widget=widgets.DataGrid(fields=player_fields)) 
     320 
     321    @expose(template="fooball.templates.team") 
     322    def team(self, team_id, *args, **kw): 
     323        player_fields = [('Name', 'name'), 
     324                         ('Birth Date', 'birthdate'), 
     325                         ('Points', 'points')] 
     326        return dict(team=Team.get(int(team_id)), 
     327                    players_widget=widgets.DataGrid(fields=player_fields)) 
     328}}} 
     329 
     330Now you can delete your {{{SimpleRosterWidget}}}, and voila! Instant table widget: 
     331 
     332***screenshot*** 
     333 
     334Note that we didn't need to change the Kid template at all.  Like our {{{SimpleRosterGrid}}}, {{{DataGrid}}} is derived from {{{widgets.Widget}}}. That means that it gets called the same way (via the display call).  It uses the {{{fields}}} parameter to decide what to display. {{{fields}}} is a list of tuples; each tuple contains the header string and either a string or a callable object (like a function, for example).  
     335 
     336If you provide a string, the {{{DataGrid}}} uses it as an attribute name on data object. If you provide a callable object, {{{DataGrid}}} calls it, passing the data item as the only parameter. The callable can return either a string (which is escaped and displayed by Kid) or an {{{Element}}} (from the elementtree library), which is rendered and then displayed. 
     337 
     338That's a mouthful. Let's figure it out by converting our hand-coded teams table on the front page to a {{{DataGrid}}}. The nicely-styled players table is making it look unfashionably plain, anyway. 
     339 
     340Add this to the imports of controllers.py: 
     341 
     342{{{ 
     343#!python 
     344from elementtree import ElementTree 
     345}}} 
     346 
     347Now, just before the Root class definition, add a function to create a link ('a') {{{Element}}} from a {{{Team}}} object: 
     348 
     349{{{ 
     350#!python 
     351def makeTeamLink(team): 
     352    link = ElementTree.Element('a', 
     353                               href='/team/%d' % team.id) 
     354    link.text = team 
     355    return link 
     356}}} 
     357 
     358Add the teams widget to the index controller. Note that I'm using the {{{makeTeamLink}}} function itself as the field value for the team name, and not a call to the function: 
     359 
     360{{{ 
     361#!python 
     362class Root(controllers.RootController): 
     363    @expose(template="fooball.templates.welcome") 
     364    def index(self): 
     365        team_fields = [('Name', makeTeamLink)] 
     366        player_fields = [('Name', 'name'), 
     367                         ('Birth Date', 'birthdate'), 
     368                         ('Team', 'team'), 
     369                         ('Points', 'points')] 
     370         
     371        return dict(teams=Team.select(), 
     372                    teams_widget=widgets.DataGrid(fields=team_fields), 
     373                    players=Player.select(), 
     374                    players_widget=widgets.DataGrid(fields=player_fields)) 
     375}}} 
     376 
     377Now just make one quick change to the welcome template: 
     378 
     379{{{ 
     380<body> 
     381<h1>International Fooball League Stats</h1> 
     382<h2>Teams</h2> 
     383${teams_widget.display(teams)} 
     384<h2>Players</h2> 
     385${players_widget.display(players)} 
     386}}} 
     387 
     388***screenshot*** 
     389 
     390That's pretty. And we've taken just about all the HTML out of our controller, which is even better. 
     391 
     392Incidentally, if the visual style of {{{DataGrid}}} looks familiar, it is: it uses the same CSS-based styling as CatWalk. If you like that style, you get it for free just by using the {{{DataGrid}}}.  If not, you can always change it in your web app's own CSS. 
     393 
     394For example, I like links to look like links, and the default CSS fragment for DataGrid removes the underline decoration.  To fix this, just add a line to your application's stylesheet: 
     395 
     396{{{ 
     397.grid td a {text-decoration:underline} 
     398}}} 
     399 
     400= Conclusion = 
     401 
     402Widgets are a powerful addition to the TurboGears tool set. They ''can'' be complex to write and use (especially when you get into the "form" and "fastdata") libraries, but they don't ''have'' to be. Wrapping custom display elements in simple widgets is quick and easy, and can help you develop faster and with fewer errors and display inconsistencies.