Adding Musicians to a Performance

Add a list of musicians to a performance. Create some functional tests of the application.

Musicians

Now it is time to start adding some content related to a given performance. We are going to start by adding musicians to a performance. For a musician, we are going to be concerned about three items:, their name, their email address, and the instrument played.

Interface

Append the following to interfaces.py:

import re
expr_keyname = re.compile('[a-zA-Z][a-zA-Z0-9_ \.]*$')
check_keyname = expr_keyname.match

expr_email = re.compile(r"^(\w&.%#$&'\*+-/=?^_`{}|~]+!)*[\w&.%#$&'\*+-/=?^_`{}|~]+"
                  r"@(([0-9a-z]([0-9a-z-]*[0-9a-z])?\.)+[a-z]{2,6}|([0-9]{1,3}"
                  r"\.){3}[0-9]{1,3})$", re.IGNORECASE)
check_email = expr_email.match

class IMusician(interface.Interface):
    """ Represents a musician involved in a distict musical performance.

    name
        The name of the musician.  This name will be used to create a
        key for the object's storage in a performance, after is has been
        sanitized of special characters and can be used as a valid, but
        readable, URL.  The user will be limited to entering
        alpha-numeric characters.
    email
        The current email address of the musician.  This may be used in
        the future to contact the musician when something changes on a
        performance that he/she should know about.  The field will be
        validated as a properly formated email address.
    instrument
        The instrument or role that the musician will have in the
        performance.  This is a free-form string that may include more
        than one instrument or role.  (i.e. "piano, vocals")

    """

    name = schema.TextLine(title=u"Name", constraint=check_keyname)
    email = schema.TextLine(title=u"Email Address", constraint=check_email)
    instrument = schema.TextLine(title=u"Instrument")

Again, we are using simple text inputs to gather the information. Notice that this time the name and email schema entries specify constraints. These constraints are checked by the regular expressions defined above the class. (These do need to precede the class definitions.)

For more information on constraints, see the Automatic Form Generation howto.

Implementation

Create a new file called musician.py in the source code directory, and add the following code:

import grok
from zope import interface
from interfaces import IMusician

class Musician(grok.Model):
    interface.implements(IMusician)
    name=u''
    email=u''
    instrument = u''

class Index(grok.EditForm):
    grok.context(Musician)
    form_fields = grok.AutoFields(Musician)

    def update(self):
        self.label = u'Edit Musician'

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

    def null_validator(self, action, data):
        return u''

    @grok.action('Cancel', validator=null_validator)
    def cancel(self, **data):
        self.redirect(self.url(self.context.__parent__.__parent__))

    @grok.action('Delete', validator=null_validator)
    def delete(self, **data):
        self.redirect(self.url(self.context.__parent__.__parent__))
        del self.context.__parent__[self.context.__name__]

class Musicians(grok.Container):
    pass

The Musician class itself should require no further explanation. Notice that, for now, we are using an EditForm for our default view. We may need to change this later when we implement permissions and roles, but for now this will suffice.

We again add an action called ‘Save’, but this time we are going to redirect to this object’s grandparent instead of its parent. The reason for this will become obvious in a moment.

There are two additional actions called ‘Cancel’ and ‘Delete’, which are used as you would expect. We are using a new parameter to the @grok.action decorator which overrides the default form validation behavior by assigning it to a new custom callable (method) named ‘null_validator’. If we didn’t override the form validation, the user would have to enter valid data before cancelling their edit or deleting a musician.

Also note how the ‘del’ command is used against the musician object by referencing the parent object. That is because the parent object will be a container just like the root object that holds the performances. We will use the same technique in the next step to allow us to remove entire performances also.

The last thing we added to this module, is a Musicians class which simply subclasses grok.Container and will be the parent container for the musician objects.

Adding to a Performance

Use your editor to once again edit performance.py. Change the content to the following:

import grok
from zope import interface
from interfaces import IPerformance
from musician import Musician, Musicians
from datetime import datetime

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

    def __init__(self):
        super(Performance, self).__init__()
        self['musicians'] = Musicians()

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

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))

    def null_validator(self, action, data):
        return u''

    @grok.action('Cancel')
    def cancel(self, **data):
        self.redirect(self.url(self.context))

    @grok.action('Delete', validator=null_validator)
    def delete(self, **data):
        self.redirect(self.url(self.context.__parent__))
        del self.context.__parent__[self.context.__name__]

class AddMusician(grok.AddForm):
    form_fields = grok.AutoFields(Musician)
    label = "Add Musician"

    @grok.action('Add')
    def add(self, **data):
        musician = Musician()
        self.applyData(musician, **data)
        keyname = musician.name.strip().replace(' ', '').replace('.', '')
        if keyname.isalnum():
            if not self.context['musicians'].has_key(keyname):
                self.context['musicians'][keyname] = musician
                return self.redirect(self.url(self.context))

We have now added an “__init__” method to the Performance class. This method acts as a constructor for the object and will be called every time a new Performance object is instantiated. (See note about “Super” below.) Notice that the constructor adds a new instance of the Musicians class to the performance’s container using the key name ‘musicians’. This name will become part of the URL that will be used to locate musicians in a given performance.

Note that the ‘Save’ action has been modified to return to the Performance view after editing, instead of redirecting to the parent list of performances. This is because we now have other interesting information to edit from the Performance view.

We also added ‘Cancel’ and ‘Delete’ actions to the performance’s edit form. However, the null_validator is only used on the Delete action, because we add new performances in an invalid state and we really want the user to add the required information instead of being able to easily cancel out of the form.

The AddMusician view inherits from AddForm and is similar in functionality to the EditForm we have used before. The musician schema validation will also be applied to the add form, however, although we allow spaces and periods in musician’s name, we do not want them in our key value or the resulting URL’s. We simply strip out the unwanted characters and then do a check to see if the key value already exists. If everything checks out, we add the new musician to the ‘musicians’ collection and redirect back to the performance page.

As with adding a new performance, we will at some point want to provide feedback if an addition fails. Later, we will allow the user to select musicians from a list which should allow us to avoid conflicts in the first place. Remember, that in this first deliverable, we are trying to provide a simple means for users to evaluate the completeness of the content objects and provide more feedback.

Viewing within a Performance

Rather than navigating to a separate form to view the musicians, we would like to incorporate the list right into the page that displays information about the performance. We will provide links to add a new musician and to edit existing ones.

To do this, we need to modify our default performance page template. Edit ~/grok/virtualgrok/music/src/music/performance_templates/index.pt to look like the following:

<html>
<head>
</head>
<body>
  <h1>Performance for: <span tal:content="view/context/__name__">Label</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>
  <br />
  <h2>Musicians</h2>
  <table class="listing" border="1" >
    <thead>
      <tr><td>Musician</td><td>Instrument</td></tr>
    </thead>
    <tbody>
      <tal:block repeat="name python:context['musicians'].keys()">
        <tr>
          <td>
            <a tal:attributes="href python:view.url(context['musicians'][name])"
            tal:content="python:context['musicians'][name].name">Musician</a>
          </td>
          <td tal:content="python:context['musicians'][name].instrument">Instrument</td>
        </tr>
      </tal:block>
    </tbody>
    <tfoot>
      <tr class="controls">
        <td colspan="2" class="align-right">
          <a href="addmusician">Add Musician</a>
        </td>
      </tr>
    </tfoot>
  </table>
</body>
</html>

The big addition is a table to list the musicians. This is similar to how we listed our performances except we are adding table rows instead of list items. Context here still refers to the current Performance object, so context[‘musicians’] refers to this performance’s own list of musicians.

Since the ‘musicians’ field is itself a container, we can iterate through its keys and assign them to a variable called ‘name’. We then use this name to access information about individual musician objects to use in our table. We will list the name and instrument fields and the name field will be a link that will allow us to edit a musician.

In the footer of our table, we place a relative link that will display the addmusician view that we added to the Performance module earlier.

Make sure the web server is running with the latest code and navigate to a performance. Click the “Add Musician” link and test out the form.

tutorials/musical_performance_organizer/AddMusician.png

When you have added some musicians, you will see them listed on the performance page. Try editing and deleting some existing musicians.

tutorials/musical_performance_organizer/MusicianList.png

Functional Tests

While working with the forms in a browser can give us some quick feedback about how our application is working (or not working), we would rather have formal testing plan that we can quickly repeat whenever we reinstall or make changes to our application. This is where functional testing is very useful.

The testing framework will find your functional tests and run them along with your unit tests. This allows you to organize them however you would like. There is a sample functional test in the main source code directory that you can extend or use as a template for your own files.

You will definitely want to visit the URL provided in the sample to read about all of the testbrowser features: http://pypi.python.org/pypi/zope.testbrowser

To create the functional tests we will simply extend the app.txt file at: ~/grok/virtualgrok/music/src/music/app.txt

The original file:

Do a functional doctest test on the app.
========================================

:Test-Layer: functional

Let's first create an instance of Music at the top level:

   >>> from music.app import Music
   >>> root = getRootFolder()
   >>> root['app'] = Music()

Run tests in the testbrowser
----------------------------

The zope.testbrowser.browser module exposes a Browser class that
simulates a web browser similar to Mozilla Firefox or IE.  We use that
to test how our application behaves in a browser.  For more
information, see http://pypi.python.org/pypi/zope.testbrowser.

Create a browser and visit the instance you just created:

   >>> from zope.testbrowser.testing import Browser
   >>> browser = Browser()
   >>> browser.open('http://localhost/app')

Check some basic information about the page you visit:

   >>> browser.url
   'http://localhost/app'
   >>> browser.headers.get('Status').upper()
   '200 OK'

The tests to append to the file:

   >>> print browser.url
   http://localhost/app

Add a new performance:

   >>> browser.getControl(None, 'NewPerformanceDate').value = '2009-03-28'
   >>> browser.getControl(None, 'SubmitButton', 0).click()
   Created New Performance: 2009-03-28
   >>> print browser.url
   http://localhost/app/2009-03-28/edit
   >>> frm = browser.getForm(None, None, None, 0)
   >>> print frm.getControl(None, 'form.location').value
   >>> print frm.getControl(None, 'form.leader').value

Check out the edit form:

   >>> frm.getControl(None, 'form.location').value = 'Where To Perform'
   >>> frm.getControl(None, 'form.leader').value = 'Who Leads'
   >>> frm.getControl(None, 'form.starttime').value = '10 AM'
   >>> frm.getControl(None, 'form.endtime').value = 'Late Afternoon'
   >>> frm.getControl(None, 'form.actions.save').click()
   >>> print browser.url
   http://localhost/app/2009-03-28
   >>> browser.getLink('Return to List').click()
   >>> link = browser.getLink('2009-03-28')
   >>> print link.text
   2009-03-28

View the display form:

   >>> link.click()
   >>> print browser.url
   http://localhost/app/2009-03-28
   >>> print browser.contents.find('Where To Perform') > 0
   True
   >>> print browser.contents.find('Who Leads') > 0
   True

Add a musician:

   >>> browser.getLink('Add Musician').click()
   >>> print browser.url
   http://localhost/app/2009-03-28/addmusician
   >>> frm = browser.getForm(None, None, None, 0)
   >>> print frm.getControl(None, 'form.name').value
   >>> print frm.getControl(None, 'form.email').value
   >>> print frm.getControl(None, 'form.instrument').value
   >>> frm.getControl(None, 'form.name').value = 'Mr. Musician'
   >>> frm.getControl(None, 'form.email').value = 'test@example.com'
   >>> frm.getControl(None, 'form.instrument').value = 'Piano'
   >>> frm.getControl(None, 'form.actions.add').click()
   >>> print browser.url
   http://localhost/app/2009-03-28
   >>> print browser.contents.find('Mr. Musician') > 0
   True
   >>> print browser.contents.find('Piano') > 0
   True

Edit the musician:

   >>> browser.getLink('Mr. Musician').click()
   >>> print browser.url
   http://localhost/app/2009-03-28/musicians/MrMusician
   >>> frm = browser.getForm(None, None, None, 0)
   >>> print frm.getControl(None, 'form.instrument').value
   Piano
   >>> frm.getControl(None, 'form.instrument').value = 'Drums'
   >>> frm.getControl(None, 'form.actions.save').click()
   >>> print browser.url
   http://localhost/app/2009-03-28
   >>> print browser.contents.find('Drums') > 0
   True

Delete the musician:

   >>> browser.getLink('Mr. Musician').click()
   >>> frm = browser.getForm(None, None, None, 0)
   >>> frm.getControl(None, 'form.actions.delete').click()
   >>> print browser.url
   http://localhost/app/2009-03-28
   >>> print browser.contents.find('Mr. Musician') > 0
   False

Delete the performance:

   >>> browser.getLink('Edit Performance').click()
   >>> frm = browser.getForm(None, None, None, 0)
   >>> frm.getControl(None, 'form.actions.delete').click()
   >>> print browser.url
   http://localhost/app
   >>> print browser.contents.find('2009-03-28') > 0
   False

As you can see, once you get used to the syntax, it is really easy to write the tests. Run ~/grok/virtualgrok/music/bin/test and you should get results like the following:

$ cd ~/grok/virtualgrok/music
$ ./bin/test
Running tests at level 1
Running unit tests:
  Running:
....
  Ran 4 tests with 0 failures and 0 errors in 0.011 seconds.
Running music.FunctionalLayer tests:
  Set up music.FunctionalLayer in 1.502 seconds.
  Running:
..........
  Ran 10 tests with 0 failures and 0 errors in 0.306 seconds.
Tearing down left over layers:
  Tear down music.FunctionalLayer ... not supported
Total: 14 tests, 0 failures, 0 errors in 1.945 seconds.

Next, we will move on to adding a song list (often called a set list) to our application.