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 Version 8 and Version 9 of IntroductionToWidgets


Ignore:
Timestamp:
01/31/07 10:49:22 (12 years ago)
Author:
jorge.vargas
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • IntroductionToWidgets

    v8 v9  
    1 = Introduction =  
    2  
    3 Widgets 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  
    7 We'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  
    9 The 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  
    11 I'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  
    13 Now, let's define our data model in {{{model.py}}}: 
    14  
    151{{{ 
    16 #!python 
    17 import datetime 
    18  
    19 class 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  
    27 class Team(SQLObject): 
    28    city = StringCol(length=20, notNull=True) 
    29    nickname = StringCol(length=20, notNull=True, alternateID=True) 
    30     
    31    players = MultipleJoin('Player') 
     2#!NewsFlash 
     3This wiki page has been migrated to the new docs site currently at  
     4http://docs.turbogears.org/1.0/IntroductionToWidgets 
    325}}} 
    33  
    34 From 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  
    36 We'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  
    50 Good enough for now. (Any resemblance to real teams, players, or final scores is purely coincidental, but I'm sure you knew that.) 
    51  
    52 Now 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  
    85 Since our template uses the {{{teams}}} and {{{players}}} variables, add them to the Root controller's index method in {{{controllers.py}}}: 
    86  
    87 {{{ 
    88 #!python 
    89 from model import Team, Player 
    90  
    91 class 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  
    100 Now, start the app, and take a look at the front page: 
    101  
    102 ***screenshot*** 
    103  
    104 Ugh.  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  
    106 Instead, we'll tell the {{{Team}}} object how to display itself by adding a string-izing method to the class: 
    107  
    108 {{{ 
    109 #!python 
    110 def __str__(self): 
    111     return "%s %s" % (self.city, self.nickname) 
    112 }}} 
    113  
    114 And 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  
    130 Much better. Now let's make pages for each team.  A new controller method will do the trick: 
    131  
    132 {{{ 
    133 #!python 
    134     @expose(template="fooball.templates.team") 
    135     def team(self, team_id, *args, **kw): 
    136         return dict(team=Team.get(int(team_id))) 
    137 }}} 
    138  
    139 With 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  
    158 And 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  
    172 And... 
    173  
    174 ***welcome screenshot*** 
    175  
    176 Ok, let's look at the team page for Pittsburgh: 
    177  
    178 ***team screenshot*** 
    179  
    180 Whoops. 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  
    182 This 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  
    186 Rather 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 
    190 from turbogears import widgets 
    191 from model import Team, Player 
    192  
    193 class 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  
    212 And 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  
    214 Let's provide the widget in the controllers: 
    215  
    216 {{{ 
    217 #!python 
    218 players_widget = SimpleRosterWidget() 
    219  
    220 class Root(controllers.RootController): 
    221     @expose(template="fooball.templates.welcome") 
    222     def index(self): 
    223         return dict(teams=Team.select(), 
    224                     players=Player.select(), 
    225                     players_widget=players_widget) 
    226  
    227     @expose(template="fooball.templates.team") 
    228     def team(self, team_id, *args, **kw): 
    229         return dict(team=Team.get(int(team_id)), 
    230                     players_widget=players_widget) 
    231 }}} 
    232  
    233 And change the welcome template to use the widget: 
    234  
    235 {{{ 
    236 <h2>Players</h2> 
    237 ${players_widget.display(players)} 
    238 }}} 
    239  
    240 ***screenshot*** 
    241  
    242 = Template Parameters =  
    243  
    244 This looks good so far. Let's do the same to the team template: 
    245  
    246 {{{ 
    247 <h2>Players</h2> 
    248 ${players_widget.display(team.players)} 
    249 }}} 
    250  
    251 ***screenshot*** 
    252  
    253 Hmm.  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: 
    254  
    255 {{{ 
    256 #!python 
    257 class SimpleRosterWidget(widgets.Widget): 
    258     template_vars=['with_team'] 
    259      
    260     def __init__(self, with_team=True, *args, **kw): 
    261         super(SimpleRosterWidget,self).__init__(*args, **kw) 
    262         self.with_team=with_team 
    263          
    264     template = ''' 
    265     <table xmlns:py="http://purl.org/kid/ns#" class="simpleplayer" border="1"> 
    266       <tr> 
    267       <th>Name</th> 
    268         <th>Birthdate</th> 
    269         <th py:if="with_team">Team</th> 
    270         <th>Points</th> 
    271       </tr> 
    272       <tr py:for="player in value"> 
    273         <td py:content="player.name"/> 
    274         <td py:content="player.birthdate"/> 
    275         <td py:if="with_team" py:content="player.team"/> 
    276         <td py:content="player.points"/> 
    277       </tr> 
    278     </table> 
    279 ''' 
    280 }}} 
    281  
    282 There's a bit going on here: 
    283  
    284  * We added a class attribute called {{{template_vars}}} ('''NOTE''' 0.9a5 will call this same attribute {{{params}}}. It will issue a {{{DeprecationWarning}}} to remind you that you should update your code).  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. 
    285   
    286  * 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. 
    287   
    288  * 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: 
    289  
    290 then inside the team template: 
    291  
    292 {{{ 
    293 <h2>Players</h2> 
    294 ${players_widget.display(team.players, with_team=False)} 
    295 }}} 
    296  
    297 ***screenshot*** 
    298  
    299 Great.  Now we're done.  
    300  
    301 But, as Mr. Jobs is so fond of saying, there's "one more thing..." 
    302  
    303 = Now, Don't Do That! = 
    304  
    305 Now 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}}}. 
    306  
    307 To use {{{DataGrid}}}, just change the {{{index}}} and {{{team}}} controller methods: 
    308  
    309 {{{ 
    310 #!python 
    311 class Root(controllers.RootController): 
    312     @expose(template="fooball.templates.welcome") 
    313     def index(self): 
    314         player_fields = [('Name', 'name'), 
    315                          ('Birth Date', 'birthdate'), 
    316                          ('Team', 'team'), 
    317                          ('Points', 'points')] 
    318         return dict(teams=Team.select(), 
    319                     players=Player.select(), 
    320                     players_widget=widgets.DataGrid(fields=player_fields)) 
    321  
    322     @expose(template="fooball.templates.team") 
    323     def team(self, team_id, *args, **kw): 
    324         player_fields = [('Name', 'name'), 
    325                          ('Birth Date', 'birthdate'), 
    326                          ('Points', 'points')] 
    327         return dict(team=Team.get(int(team_id)), 
    328                     players_widget=widgets.DataGrid(fields=player_fields)) 
    329 }}} 
    330  
    331 Now you can delete your {{{SimpleRosterWidget}}}, and voila! Instant table widget: 
    332  
    333 ***screenshot*** 
    334  
    335 Note 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).  
    336  
    337 If 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. 
    338  
    339 That'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. 
    340  
    341 Add this to the imports of controllers.py: 
    342  
    343 {{{ 
    344 #!python 
    345 from elementtree import ElementTree 
    346 }}} 
    347  
    348 Now, just before the Root class definition, add a function to create a link ('a') {{{Element}}} from a {{{Team}}} object: 
    349  
    350 {{{ 
    351 #!python 
    352 def makeTeamLink(team): 
    353     link = ElementTree.Element('a', 
    354                                href='/team/%d' % team.id) 
    355     link.text = team 
    356     return link 
    357 }}} 
    358  
    359 Add 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: 
    360  
    361 {{{ 
    362 #!python 
    363 class Root(controllers.RootController): 
    364     @expose(template="fooball.templates.welcome") 
    365     def index(self): 
    366         team_fields = [('Name', makeTeamLink)] 
    367         player_fields = [('Name', 'name'), 
    368                          ('Birth Date', 'birthdate'), 
    369                          ('Team', 'team'), 
    370                          ('Points', 'points')] 
    371          
    372         return dict(teams=Team.select(), 
    373                     teams_widget=widgets.DataGrid(fields=team_fields), 
    374                     players=Player.select(), 
    375                     players_widget=widgets.DataGrid(fields=player_fields)) 
    376 }}} 
    377  
    378 Now just make one quick change to the welcome template: 
    379  
    380 {{{ 
    381 <body> 
    382 <h1>International Fooball League Stats</h1> 
    383 <h2>Teams</h2> 
    384 ${teams_widget.display(teams)} 
    385 <h2>Players</h2> 
    386 ${players_widget.display(players)} 
    387 }}} 
    388  
    389 ***screenshot*** 
    390  
    391 That's pretty. And we've taken just about all the HTML out of our controller, which is even better. 
    392  
    393 Incidentally, 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. 
    394  
    395 For 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: 
    396  
    397 {{{ 
    398 .grid td a {text-decoration:underline} 
    399 }}} 
    400  
    401 = Conclusion = 
    402  
    403 Widgets 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.