Infrastructure Perspective: Output Objects

Introduction

O
utput objects stand as an interface between the code that generates the content for the site and the raw HTML that gets sent back to the browser. I hate seeing code like this:
  print "<table>"
  print "<tr>"
  print "<td>foo</td>"
  print "<td>bar</td>"
  etc.
because it's very difficult to ensure proper balancing of tags, and thus very easy to generate incorrect HTML (the kind that some browsers render correctly and others don't).

Output objects provide a way to build an entire page dynamically, before sending a single line of output to the browser. This has several advantages. First, if at the last minute the script discovers an error, it can throw out the nearly-complete page and produce a new page which notes the error condition. Second, the page need not be constructed from the top to the bottom, but can be built piecemeal as the script gathers and processes information.

The Output Class Hierarchy

E
very Output class is a subclass of Output, and has a single method: render(). This method returns the HTML representation of the object as a string.

Rendering is triggered by calling the render_output(oo) function in the utils.output module, with an output object oo as argument. This function accepts output objects, lists of output objects, and plain old Python strings (which are returned verbatim). This makes it possible to include strings in the output without a special wrapper class.

The thing that makes Output objects so powerful is that most of them are containers. Container is a subclass of Output which adds a member variable, suboutputs. This variable contains a list of output objects that are "inside" the container. A container's render method concatenates the results of rendering each of the container's suboutputs. The Container class also has all the functions of a Python list, such as c.append(foo), len(c), c.insert(0, bar), etc.

Containers are not particularly useful for wrapping tags around some text: it's usually easier to just put three output objects into the parent container: the start tag, the text, and the end tag. (opposite day? NAR)

Containers are useful for more complicated formatting. The EnglishList class (in utils/output/html.py) provides an instructive example. This Container subclass formats its suboutputs as an English list, by putting commas and the word `and' between individual suboutputs. This class is a simple, complete abstraction of the rules for English lists, freeing other code from the burden of (re)implementing these rules.

List Elements

T
wo classes exist to make HTML lists:
DL
The DL class makes a definition list. It wraps its suboutputs alternately in <dt> and <dd> tags.
UL
The UL class makes an unordered list. It inserts an <li> tag before each suboutput.

Form Elements

T
he FORM container creates an HTML form. Its action is hard-coded to POST, and its encoding type (enctype) is hard-coded as multipart/form-data. If you find either of these hard-codings problematic, re-examine your motivations: the structure of URLs on this site means a form submitted with GET has its contents thrown into internal--not an appropriate place for user-generated data.

The FORM constructor takes a keyword argument, action=. If this argument is a string, it is used as the action attribute of the <form> tag. If it is a Display object, then the form will submit to the action URL for that object.

There are several Output subclasses representing form controls. All but one are subclasses of INPUT, the constructor for which takes a single argument, the attrs dictionary. The dictionary specifies the attributes of the generated tag. So, for instance, the tag <input type="text" name="foo" value="bar"> would be represented as TEXT_INPUT(attrs={'name' : 'foo', 'value' : 'bar'}). The subclasses are:

  • TEXT_INPUT
  • SUBMIT_INPUT
  • RESET_INPUT
  • RADIO_INPUT
  • PASSWORD_INPUT
  • IMAGE_INPUT
  • HIDDEN_INPUT
  • FILE_INPUT
  • CHECKBOX_INPUT
  • BUTTON_INPUT

Finally, the SELECT class creates <select> tags within a form. Its constructor takes the following keyword arguments:

name
The name attribute of the control.
options
A list of pairs (n, k) where n is the name (seen by the user) and k is the key (seen by the program).
default
The key or list of keys which should be selected initially.
size
The number of rows in a multiple select control.
multi
Boolean: true means this is a
otherwise it's a
attrs
Any additional attributes for the <select> tag.

Stylistic Elements

T
here are a few stylistic containers available: EnglishList, BulletColumnsContainer, and DropCap. EnglishList is described above. BulletColumnsContainer makes a two-column, bulleted list of its suboutputs. It's used for listing members of projects, laboratories, and the like. DropCap will make a drop cap of the first letter from its suboutputs. It's used in this documentation, just for kicks.

Table Elements

H
ere's where things start to get complicated. When we design a table, it is convenient to specify all of its attributes -- for the <table>, <tr>, and <td> tags -- in one place. But we would also like to use separate objects to represent each of the three tags.

The TABLE, TR, and TD classes all work together. The main attributes are specified to the TABLE constructor, which has the following keyword arguments:

attrs
The attributes for the <table> tag, as a dictionary.
row_attrs and row_loop_start
A list of attributes for the <tr> tags. The first list element is applied to the first row, the second to the second row, and so on. When the end of the list is reached, processing goes back to the list element with index row_loop_start. See below for an example.
col_attrs and col_loop_start
Like row_attrs, but apply to sequential <td> tags instead.
sub
Any pre-specified suboutputs (not usually used).

As an example of the use of this class, this Python code:

table = TABLE(
  attrs = {'border' : 0, 'cellspacing' : 0 },
  row_attrs = [ {'bgcolor' : '#808080'},
                {'bgcolor' : '#FFFFFF'},
                {'bgcolor' : '#EEEEEE'} ],
  row_loop_start = 1,
  col_attrs = [ { 'bgcolor' : '#808080' },
                { 'width' : '100' } ],
  col_loop_start = 1)
table.append(TR( [ 'Foo', 'Bar', 'Bing', 'Baz' ] ) )
for i in range(10):
  table.append(TR( [ 'foo-%d' % i, 'bar-%d' % i,
                     'bing-%d' % i, 'baz-%d' % i ] ) )
page.append(table)

made this table:

Foo Bar Bing Baz
foo-0 bar-0 bing-0 baz-0
foo-1 bar-1 bing-1 baz-1
foo-2 bar-2 bing-2 baz-2
foo-3 bar-3 bing-3 baz-3
foo-4 bar-4 bing-4 baz-4
foo-5 bar-5 bing-5 baz-5
foo-6 bar-6 bing-6 baz-6
foo-7 bar-7 bing-7 baz-7
foo-8 bar-8 bing-8 baz-8
foo-9 bar-9 bing-9 baz-9

NestContainer

O
K, that wasn't so complicated. This is. Occasionally we want to have a whole bunch of stuff in the same kind of container, but occasionally other things should intervene. Specifically, we need this functionality to put all of the names on the main people page, with the categories headlines interspersed. To represent the desired containment hierarchy graphically:
  • Container:
    • String: <h3>Faculty</h3>
    • BulletColumnsContainer:
      • Professor A
      • Professor B
      • Professor C
      • etc.
    • String: <h3>Masters in Computer Science Program</h3>
    • BulletColumnsContainer:
      • Instructor A
      • Instructor B
      • Instructor C
      • etc.
    • etc.

In this example, we have a BulletColumnsContainer nested within a Container. Most of the stuff that gets inserted into this nesting (people's names) ends up inside of a BulletColumnsContainer, but the occasional item (a headline) is outside of any BulletColumnsContainer, just in the Container.

We use NestContainer to accomplish this. A NestContainer object is constructed with a list of constructors for the different levels of containment. In the example above, it would be constructed like this:

nc = NestContainer(Container, BulletColumnsContainer)
the object is then used like any other container (with the exception that it only supports the append method; this does not turn out to be a problematic restriction, and makes for more efficient code). When an object is inserted into the NestContainer (via nc.append(obj)), it is placed inside of a BulletColumnsContainer which itself is inside a Container which is inside whatever contains nc.

To convince NestContainer to insert an object higher up the containment hierarchy, use the NestContainer.Out(object, up) function. object is the object to be inserted, and up is the number of levels up through the containment hierarchy that the object should be inserted. So to insert the headlines in the example above (which are one level up from normal), we would use

nc.append(NestContainer.Out("<h3>Faculty</h3>", 1))

So the constructed containment hierarchy looks like this:

  • NestContainer:
    • NestContainer.Out("<h3>Faculty</h3>", 1)
    • Professor A
    • Professor B
    • Professor C
    • etc.
    • NestContainer.Out("<h3>Masters in Computer Science Program</h3>", 1)
    • Instructor A
    • Instructor B
    • Instructor C

NestContainer takes care of turning that into the hierarchy described above.

This class is hard to understand without seeing it in action. Check out how it's used in the people page.

The CSPage Container

T
he top-level container, which is constructed by the Dispatch handler, is an object of the CSPage class. It is responsible for adding the characteristic computer science header and footer.

The page object has a number of methods, useful for manipulating its appearance:

page.set_type(t)
Set the type, or section, that this page is in. t should be one of info, people, courses, research, or events.
page.set_title(t)
Set the title of the page (affects the <title> tag).
page.add_navigation(url, text)
Add a navigation link to url with name text.
page.add_mode(url, text)
Add a mode link to url with name text.
page.add_help(url, text)
Add a help link to url with name text. Help links are currently unused, but will appear in the yellow bar just like navigation and mode links.

Finding the Files

A
ll of the output class files are in the directory utils/output.