Adding Performances

Create a basic content object which describes a Performance. Also, create a page template and the necessary view code so that a new performance can be added using a web browser.

The Performance Class

It is time to create our first content object. If we review the entity outline, we can see that the Performance object is composed of both simple data fields and collections of other objects. We are going to start by implementing the simple string fields of “Location” and “Leader”. The performance date will already be encoded as the object’s key in the container object.

Since this object will also be a container, we will have it inherit from the grok.Container class also. This will provide a basic content object that will be easy to store in the object database and to render into an HTML page.

Before we work on the performance class directly, we are going to define its interface. Python doesn’t generally use interfaces in the way that other languages do. They are used quite a bit in Zope programming for a variety of reasons. The reason we will use them right now, is to allow the auto-generation of forms and validation of user input. Later, you will find that they are valuable when adapting or combining different content objects to present their information in different ways without having to rework existing code.

Interfaces can also be an excellent way to document your code. By gathering them together in one place and surrounding them with meaningful comments, someone reading your code can get a quick overview of what your objects are going to do without having to read through all the implementation details.

For that reason we are going to gather all of our interfaces into one file called “interfaces.py” in the ~/grok/virtualgrok/music/src/music directory. Using your editor, create this file with the following content:

import grok
from zope import interface, schema

class IPerformance(interface.Interface):
    """ Represents a distict musical performance.

    This object defines the top level of our content.
    It will also "contain" other collections of content objects.

    location
        A free-form explaination of where the performance will occur.
    leader
        The name of the person co-ordinating a particular performance.
    starttime
        The time the performance is scheduled to begin.
        This information will be an optional, free-form string.
    endtime
        The time the performance is scheduled to end.
        This information will be an optional, free-form string.

    """

    location = schema.TextLine(title=u"Location")
    leader = schema.TextLine(title=u"Leader")
    starttime = schema.TextLine(title=u"Start Time", required = False)
    endtime = schema.TextLine(title=u"End Time", required = False)

All we have done so far is define that a performance will have a location and a leader. The “schema” assignments inform the rendering machinery that these two items can be represented as text lines on forms. We also added two optional string fields for the start and end time of the performance.

Grok allows you to auto-generate three types of forms: display forms, add forms, and edit forms. For add and edit forms, these fields will be rendered as text inputs. For display forms, the content of the field will be output as part of the HTML content.

The title attributes specify the label that will be associated with the HTML control. You can find more about schemas in the Working with Forms in Grok tutorial.

Now create a file called performance.py in the same directory, with the following content:

import grok
from interfaces import IPerformance
from zope import interface

class Performance(grok.Container):
    interface.implements(IPerformance)
    location = u''
    leader = u''
    starttime = u'--'
    endtime = u'--'

class Index(grok.DisplayForm):
    form_fields = grok.AutoFields(Performance)

Notice how this class uses the interface.implements directive to associated itself with the IPerformance interface. Also, note that we added a simple display form using grok.AutoFields and gave it the Performance class as a parameter.

When there is only one model class in a module (.py file), a view or form class will automatically associate itself with that class. In other words, that model class will become the view class’s context. (You will hear and use this idea of “context” frequently.)

If the name of the view to be used in the URL is not explicitly stated, it will be the name of the view or form class in lowercase. When the name is “index”, it will be the default view for the content object.

For the Performance object, the auto-generated Display Form will be used to render the contents when the URL specifies its key value in the container.

Now that we have a Performance object to add, we must revisit the Music class and enable its addition to the container.

Adding a Performance

As of yet, we have not seen anything rendered to HTML except the welcome screen generated by grokproject. First, we need to modify the template used to render the page. The main page of the application needs to list the available performances and provide a way to add new performances.

We will create an HTML list of the performances containing links to the URL of each performance. For now, we will provide a text box for the user to enter the date of a new performance. (Later we will implement a “date picker” control.) Since we know that we can limit the choices for the “hour” portion of the performance key to one of twenty five choices (“None” or -00 thru -23), we will provide an option box for this entry.

Edit ~/grok/virtualgrok/music/src/music/app_templates/index.pt as follows:

<html>
<head>
</head>
<body>
  <h1>Performances</h1>

  <p>Available Performances:</p>
  <ul>
    <li tal:repeat="key python:context.keys()">
        <a tal:attributes="href python:view.url(key)" tal:content="python:key">Performance Date</a>
    </li>
  </ul>

  <form tal:attributes="action view/url" method="POST">
    New Performance Date: <input type="text" size="15" label="whatnot" name="NewPerformanceDate" value="" /> (Format: yyyy-mm-dd)
    <br />
    New Performance Hour:
    <select name="NewPerformanceHour" >
        <option value="">--</option>
        <option value="-00">00 / 12AM</option>
        <option value="-01">01 /  1AM</option>
        <option value="-02">02 /  2AM</option>
        <option value="-03">03 /  3AM</option>
        <option value="-04">04 /  4AM</option>
        <option value="-05">05 /  5AM</option>
        <option value="-06">06 /  6AM</option>
        <option value="-07">07 /  7AM</option>
        <option value="-08">08 /  8AM</option>
        <option value="-09">09 /  9AM</option>
        <option value="-10">10 /  10AM</option>
        <option value="-11">11 /  11AM</option>
        <option value="-12">12 /  12PM</option>
        <option value="-13">13 /  1PM</option>
        <option value="-14">14 /  2PM</option>
        <option value="-15">15 /  3PM</option>
        <option value="-16">16 /  4PM</option>
        <option value="-17">17 /  5PM</option>
        <option value="-18">18 /  6PM</option>
        <option value="-19">19 /  7PM</option>
        <option value="-20">20 /  8PM</option>
        <option value="-21">21 /  9PM</option>
        <option value="-22">22 /  10PM</option>
        <option value="-23">23 /  11PM</option>
    </select>
    (Optional)
    <br />
    <input type="submit" value="Add New Performance" name="SubmitButton" />
  </form>
</body>
</html>

For the most part this template is a simple HTML form. They key area to note is the unordered list section and the “tal:” attributes it contains. We are stating that we want to repeat list items for each of the items in the “context object’s” list of keys. (Here the “context object” will be the Music class, because that is the model class associated with the Index view in the app.py module shown below.)

We assign the value of each key to a variable called “key” and use that to generate the displayed text and the href attribute of the link tag.

We now need to modify the view (the Index class) used to render the Music class to interact with this template.

Use your editor to modify ~/grok/virtualgrok/music/src/music/app.py to the following:

import grok
from datetime import datetime
import time
from performance import Performance #Note that we now import the Performance object in this module.

class Music(grok.Application, grok.Container):
    def IsValidKey(self, keyVal):
        if not isinstance(keyVal, basestring):
            return False
        if len(keyVal) in [10, 13]:
            keyParts = keyVal.strip().split('-')
            if len(keyParts) in [3, 4]:
                try:
                    datePart = '-'.join([keyParts[0], keyParts[1], keyParts[2]])
                    newDate = datetime(*(time.strptime(datePart, '%Y-%m-%d')[0:6]))
                except ValueError:
                    return False

                if len(keyParts) == 4:
                    try:
                        newTime = int(keyParts[3])
                        if newTime < 0 or newTime > 23:
                            return False
                    except ValueError:
                        return False

                # Everything checks out.
                return True

        return False


class Index(grok.View):
    def update(self, SubmitButton=None, NewPerformanceDate=u'', NewPerformanceHour=u''):
        # Check if the submit button was clicked.
        if SubmitButton == 'Add New Performance':
            # Combine the contents of the two form fields to create a possible key.
            NewPerformanceKey = NewPerformanceDate + NewPerformanceHour
            # Check if the new key is valid.
            if self.context.IsValidKey(NewPerformanceKey):
                # Check if the key already exists in the container.
                if not self.context.has_key(NewPerformanceKey):
                    # Add a blank performance to the container.
                    self.context[NewPerformanceKey] = Performance()
                    # Output a confirmation note to the console.
                    print 'Created New Performance: ' + NewPerformanceKey
                    # Redirect the browser to the URL of the new page.
                    self.redirect(self.url(NewPerformanceKey))

The Index class inherits from grok.View, which will give it the ability to access model objects and render a template. Its context will be the Music class and it will attempt to use a template called index.pt (which we edited previously).

The update method runs every time the view is accessed and its parameters are automatically filled from the values of form controls in the template. The parameter names simply have to match the “name” attributes of the form controls.

We call the IsValidKey method on the Music class by referring to it through “self.context”. If the key is valid and unique, we add a new Performance object to the container using that key and redirect to the URL of the new object. All the details of persisting the new object in the database are handled automatically.

Run the project and try adding a new performance for “2009-03-15”.

tutorials/musical_performance_organizer/AddPerformanceForm.png

If you look at console where you are running the application you should have an output that says, “Created New Performance: 2009-03-15”. Your browser should move to a rather uninteresting screen that simply says, “Location”, “Leader”, “Start Time”, and “End Time”. These are the titles that where taken from the schema defined in the IPerformance interface. Since we created a blank performance object, the values for these fields are empty strings and dashes and we do not yet have a way to edit them.

Note the URL of our new Performance: http://localhost:8080/band/2009-03-15 The new part of the URL is this object’s key value in the container. It follows the format specified in the requirements and is easy to understand.

We do not yet have a way to get back to the main application page. Instead of hitting the “back” button, browse to http://localhost:8080/band directly and see that we now have an entry listed under “Available Performances”.

tutorials/musical_performance_organizer/FirstListedPerformance.png

Go ahead and try adding more performances with invalid or duplicate dates. Confirm that they are not added. (We will at some point need to give the user some feedback when an attempt to add a performance fails.)

Editing a Performance

If we want to be able to edit the two fields currently in our performance objects, we can do it by adding two lines of code to performance.py.

class Edit(grok.EditForm):
    form_fields = grok.AutoFields(Performance)

This will create an editable form view that is accessible by appending “/edit” to the URL of the performance object. We currently do not have a link to take you to this URL, so you will have to adjust it manually.

tutorials/musical_performance_organizer/FirstEdit.png

Try typing some text into the fields and press the “Apply” button. The asterisks mean the fields are required. Try leaving one of the required fields blank and note the validation error. Browse back to the display form and see that your changes have been applied.

While that is quite a bit of functionality from two lines of text, let’s make this editing process work a little smoother and look nicer.

We will start by adding a bit more to the edit view so that we have some visual context of which performance we are working on. We will also override the default behavior of the form post action to give the button a new name and redirect the user to the application main page when editing is complete.

class Edit(grok.EditForm):
    form_fields = grok.AutoFields(Performance)

    def update(self):
        self.label = u'Edit Performance ' + self.context.__name__

    @grok.action('Save')
    def edit(self, **data):
        self.applyData(self.context, **data)
        self.redirect(self.url(self.context.__parent__))

The update method gets called any time the view is rendered. The self.label property will fill a heading in the default EditForm with the description that includes the performance name.

The @grok.action directive overrides the name of the button used to post the form. The applyData method is a quick way to map all of the form fields in the post request to the context of this view, which is the Performance object.

Finally, we redirect to the Performance object’s parent, which is the Music application container object.

We can make our display form look a little nicer by adding a custom template. We are basically taking the default template from the grokcore.formlib package which can be found in your “buildout-eggs” cache directory and modifying it slightly to include a heading and links for editing and returning to the parent page.

Save the following in a file called “index.pt” in a new directory called ~/grok/virtualgrok/music/src/music/performance_templates.

<html>
<head>
</head>
<body>
  <h1>Performance for: <span tal:content="view/context/__name__">Unique Date</span></h1>
  <table class="listing" border="1" >
    <tbody>
      <tal:block repeat="widget view/widgets">
        <tr tal:define="odd repeat/widget/odd"
          tal:attributes="class python: odd and 'odd' or 'even'">
          <td class="fieldname" align="right">
            <tal:block content="widget/label"/>:
          </td>
          <td>
            <input tal:replace="structure widget" />
          </td>
        </tr>
      </tal:block>
    </tbody>
    <tfoot>
     <tr class="controls">
        <td colspan="2" class="align-right">
          <a href="edit">Edit Performance</a>
        </td>
      </tr>
      <tr class="controls">
        <td colspan="2" class="align-right">
          <a href="../">Return to List</a>
        </td>
      </tr>
    </tfoot>
  </table>
</body>
</html>

We need to instruct our display form to use this template by changing the Index view of the performance module to the following:

class Index(grok.DisplayForm):
    template = grok.PageTemplateFile('performance_templates/index.pt')

Finally, we want to edit the last line of our Index view code for the Music class so that when we add a new performance we are taken directly to the edit form.

class Index(grok.View):
    def update(self, SubmitButton=None, NewPerformanceDate=u'', NewPerformanceHour=u''):
        if SubmitButton == 'Add New Performance':
            NewPerformanceKey = NewPerformanceDate + NewPerformanceHour
            if self.context.IsValidKey(NewPerformanceKey):
                if not self.context.has_key(NewPerformanceKey):
                    self.context[NewPerformanceKey] = Performance()
                    print 'Created New Performance: ' + NewPerformanceKey
                    # Redirect the browser to the URL of the new object's edit page.
                    self.redirect(self.url(NewPerformanceKey) + '/edit')

We now have the beginnings of our application. In the next section, we will start adding some collections of other objects to our performance objects and begin writing some functional tests.