Adding Songs to a Performance

Add a list songs to a performance. Prepare for uploading files that will be linked to the songs.

Songs

We are now going to put together the set list for our performance. The first part of this will be similar to how we added the musicians. Later, we will have to add the ability to upload and attach documents related to items in the set list.

Interface

Append the following to interfaces.py:

class ISong(interface.Interface):
    """ Represents a song in the set list for a performance.

    title
        The title of the song.  This may include author information, if
        that is useful in identifying the song.  This title 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
        part of a valid, but readable, URL.
    key
        The key the song will be performed in.  This is a free-form text
        field.
    arrangement
        Notes on how the song will be arranged or the key it will be
        played in.  Examples might be,
        "Verse 1, Chorus, Verse 2, Chorus, Bridge, Chorus"
        or "V1,Ch,V2,Ch,Br,Ch."  It can be any free-form string that
        the musicians will understand.
    order
        The order the songs will be performed within the set.  Also the
        display order of the item within the set list.  This will be an
        integer value.
    """

    title = schema.TextLine(title=u"Song")
    key = schema.TextLine(title=u"Key", max_length=20, required=False)
    arrangement = schema.TextLine(title=u"Arrangement Notes", required=False)
    order = schema.Int(title=u"Order", default=0)

You can see description of the fields we are adding for each song and the constraints placed on the input.

Model and View Classes

Create a new file in the “src” directory and name it “song.py”. Paste the following code into the file:

import grok
from zope import interface
from interfaces import ISong
from zope.app.form.browser.textwidgets import TextWidget

class Song(grok.Container):
    interface.implements(ISong)
    title=u''
    key=u''
    arrangement=u''
    order = 0

class LongTextWidget(TextWidget):
    displayWidth = 50

class Index(grok.EditForm):
    grok.context(Song)
    form_fields = grok.AutoFields(Song)
    form_fields['title'].custom_widget = LongTextWidget
    form_fields['arrangement'].custom_widget = LongTextWidget

    def update(self):
        self.label = u'Edit Song for ' + self.context.__parent__.__parent__.__name__

    @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 SetList(grok.Container):
    pass

Because the “Title” and “Arrangement” fields can be quite long, we want to be able to display a larger text box for user input. In order to do this, we need to override the standard TextWidget by creating our own LongTextWidget with a display width of 50 characters. The rest of this code should be familiar from the work we did previously.

Now, we must prepare the Performance class to work with the new song objects.

Update Performance

Near the top of “performance.py”, add the following line:

from song import Song, SetList, LongTextWidget

The Performance class also needs to be modified to add a container to hold the songs:

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()
        self['setlist'] = SetList()

Note: This adds a new container for songs to any new performance object. You will only be able to add songs to Performances created after you have added this code. (If this system was already in use and needed to be updated, we would have to write the code differently to allow for updating old performances.) Since we a still developing, it might be best to delete and recreate your application instance using the Admin UI.

Append the following to the bottom of “performance.py”:

class AddSong(grok.AddForm):
    form_fields = grok.AutoFields(Song)
    form_fields['title'].custom_widget = LongTextWidget
    form_fields['arrangement'].custom_widget = LongTextWidget
    label = "Add Song"

    def cleankey(self, dirtykey):
       cleanstring = []
       padded = False
       for c in dirtykey.strip(' _'):
           if c.isalnum():
               padded = False
               cleanstring.append(c)
           elif not padded:
               if c == ' ':
                   padded = True
                   cleanstring.append('_')
               elif '_-'.find(c) > -1:
                   padded = True
                   cleanstring.append(c)
       return ''.join(cleanstring).strip(' _')


    @grok.action('Add')
    def add(self, **data):
        song = Song()
        self.applyData(song, **data)
        keyname = self.cleankey(song.title)
        if not self.context['setlist'].has_key(keyname):
            self.context['setlist'][keyname] = song
            return self.redirect(self.url(self.context))

Before adding the new song to the set list, a key is generated by calling a method that is part of the add song class. This method should remove or replace any characters that we would not want to be part of the URL for this song.

Viewing within a Performance

Similar to the musician list, we would like to incorporate the list of songs right into the page that displays information about the performance. We will provide a links to add a new song 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>
  <style type="text/css">
    table { empty-cells:show; }
  </style>
</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>
  <br />
  <h2>Set List</h2>
  <table class="listing" border="1" >
    <thead>
      <tr><td>#</td><td>Songs</td><td>Key</td><td>Arrangement</td></tr>
    </thead>
    <tbody>
      <tal:block repeat="song python:sorted(context['setlist'].values(), key=lambda obj:obj.order)">
        <tr>
          <td tal:content="python:song.order">#</td>
          <td>
            <a tal:attributes="href python:view.url(song)"
            tal:content="python:song.title">Title</a>
          </td>
          <td tal:content="python:song.key">Key</td>
          <td tal:content="python:song.arrangement">Arrangement</td>
        </tr>
      </tal:block>
    </tbody>
    <tfoot>
      <tr class="controls">
        <td colspan="4" class="align-right">
          <a href="addsong">Add Song</a>
        </td>
      </tr>
    </tfoot>
  </table>
</body>
</html>

Notice the new table at the end of the template used to display the songs. Since we do not want the songs sorted alphabetically (as they would if we used tried to use the key value derived from the title), but by the value of the “order” field, we need to make this happen.

We could create a new method on our performance model object or in the view to return a sorted list of songs. Instead, we accomplish this right in the template by using Python’s built-in “sorted” function. The “lambda” operator allows us to specify the sort condition directly without having to created a new named function. (See the Python Sorting Mini-HOW TO.)

Start the application and create a new performance. The new performance should have a “Set List” section. You should be able to add and edit songs using a form that looks similar to the following:

tutorials/musical_performance_organizer/AddSong.png

Note: If you have problems viewing previously created performances, you may need to reset your application by going to the Grok AdminUI to delete and re-create your test application with the same application name. If we were upgrading an application that was already in use by others, we would create the code necessary to migrate existing stored objects to the new application. Since we are developing the first version of this application, it is easier to recreate our test objects as needed.