Personal tools
You are here: Home Documentation Tutorials Musical Performance Organizer - Part 1

Musical Performance Organizer - Part 1

Note: Return to tutorial view.

The first in a series of tutorials that will build a reasonably complex application, starting from a simple prototype and evolving it into a finished application. The tutorial will touch on many practical aspects of building the application and demonstrate how to combine the various components that are available in Grok into a finished product.

Introduction

An overview of the example requirements and a road map for how the tutorial will cover the development of the application.

General Overview

One of the goals of this tutorial is to give the reader a chance to walk through the process of developing a Grok application. Specific pieces of application development are handled in greater details in other how-to's and tutorials. In this tutorial, we hope to demonstrate how the pieces can come together in a development process.

If you are a seasoned Zope3 developer or even a long-time Python developer, you probably have your own preferred development style and you may disagree with some parts of this tutorial. (If you have ideas for improvements, they are certainly welcome but this is not an exercise in determining which development style, text editor, IDE, template system, or AJAX library is the best or most efficient.)

This tutorial is targeted at those who are new to the Grok and Zope3 framework. They may have been using other web development frameworks and programming languages, or they may have been lucky enough to have found Grok first and are anxious to see what it can do for them.

We will try to not make assumptions about your knowledge of Python, Zope3, and Grok. Obviously, it is assumed that you have some software development experience and are able to learn the Python language and follow the development patterns. A great deal of high quality documentation and tutorials exists for most of the topics surrounding this tutorial. Where appropriate we will provide links to resource materials.

It is expected that you have Python version 2.6 installed on your computer. Specifically, a version of CPython in the 2.6.x series from http://www.python.org, not Jython, IronPython, or some other alternative implementation of the language. Python version 2.4 and 2.5 are also acceptable if you are running on Linux, however, version 2.6 is required for Windows and version 3.0 will not currently work on any platform.

Grok will work on any operating system supported by Python which includes Linux, Macintosh, and Windows. There can be some slight differences in things like directory locations depending on platform. We will try to note some of the differences, but understand that Ubuntu Linux is being used for the writing of this tutorial.

Sample Application Requirements

Description

In some venues, ad-hoc bands of musicians and vocalists come together to perform at scheduled events. The band leader for a given event needs to organize the people involved, develop the list of music they will perform, and distribute sheet music, chord charts, and lyrics.

A leader may be in charge of several performances involving different musicians. Likewise, musicians and vocalists may be involved in several performances lead by different people.

Currently, much of this organization is done via email. As expected, often key people are missed in the email distribution or their email address changes or their account is broken, causing them to miss important information.

Even if they receive all the information, it is still possible for them to mis-file the emails or attachments. Often updates for various performances come "out of sequence" and, if the subject is not very clear (i.e. "Friday's Performance" instead of a distinct date), the recipient can get confused.

Having a web site that lists all the important information and allows them to download the files they need for their preparation would be a benefit to everyone involved.

Entity Outline

  • Application Root (contains Performance objects)
    • Performance
      • Date
      • Location
      • Leader
      • Start and End Times (free-form, optional)
      • Musicians (contains Musician objects)
        • Name
        • Email Address
        • Instrument
      • Set List (contains Song objects)
        • Song Title
        • Arrangement Notes
        • Display Order
        • Attachments (contains Resource references)
          • File Name
          • File URL
          • File MD5 Hash (Hidden/Optional)
      • Comments (Contains Comment objects
        • Date
        • Author Email Address
        • Comment Text

General Requirements

  • Performances should be organized by date. Since it is possible to perform more than once in a day, an optional, "rough time" can be added to the day to differentiate them. Performances will never happen more frequently than once per hour.
  • Performances should be able to be found by URL's that can be understood to refer to a certain performance. Example: http://example.com/TheBandName/2009-03-05/
  • Users should be able to upload files in any format they choose (PDF, DOC, Text, etc). No special handling or MIME types are provided by the application to enhance browser interaction.

Phased Deliverables

Phase 1 - Basic Functions
  • All current and future performances are listed Sorted ascending by date.

  • Text-field forms

    User hand-types dates, musician names, song names, and display order relying on web browser for any form completion efficiencies.

  • Basic file upload support

    Upload file attachments to a static directory or directly into the storage database. Always upload user selected attachments and use a "name-chooser" to prevent overwriting existing files. Deleting an attachment, or the song which contains it, should remove the file from the application, but may require a management operation to reclaim the free space.

  • No user authentication, roles or permission support.

    Rely on external security via web proxy configuration or rewrite rules.

  • Basic Text Comments Anyone can comment.

    No email support or comment-spam protection.

Phase 2 - Improved Usability
  • Enhanced Performance Listing

    Ability to see past performances. More display of summary performance details such as simple musician and song listings.

  • Form Widgets

    Use of date-picker, and drop down lists to speed selection of information that already exists in the application. Buttons or "Drag and Drop" to adjust display order.

  • Enhanced Upload

    Ability to quickly select more than one attachment item. Warn user when an attachment may already exist and offer to link to the existing file rather than uploading another. (This could be done by file name matching or possibly a file content hash comparison.) Only remove underlying file content when no attachments (in any performance) reference the file.

  • Basic Roles for Editing

    Leader role can create Performances and add songs to Set list. Assistant role can modify existing song arrangement notes and add/remove attachments.

  • Email Support for Comments

    Basic comment-spam protection via capcha or email confirmation request. Notification email with URL of this performance sent to the addresses of all associated musicians.

Phase 3 - Style and Management
  • Create a stylized look for the site.

    Use a template system, CSS, and some images to give the site an appealing look. Should be easy to modify the colors and any logos via CSS edits.

  • Application Search

    Ability to search or filter performances by song performed or musicians involved.

  • Provide Management Functions

    Removal or archiving of past performances. Attachment management to free storage resources.

Development Road Map

We are going to begin by getting the basic development environment in place and test that it works. We will then begin writing some tests which correspond to some of the Phase 1 requirements. From there we will begin to write model and view code to satisfy our tests.

Several iterations will be required to get through the first deliverable. From there, we will move on to the other deliverables.

It will take a significant amount of time to make it all the way through this tutorial, but we will try the keep the amount of work in each iteration manageable.

Setting Up The Environment

Configuring the project environment, understanding where files are located, and how to run application and test code.

Installing Grok

Instructions for installing Grok can be found in the Grok Tutorial . We are going to use a helper application called "GrokProject" to set up our sample project. This is the recommended way to install a Grok application as it puts in place all the scripts and tools you will need to build, run and deploy your application.

GrokProject is installed by running a tool called "easy_install", which will download GrokProject from a repository on the internet called PyPI (the Python Package Index). Most of the application code that you will install using Grok will come from this source.

While it is very easy to use easy_install, there can be problems if you use it to install all the software you need for your Grok project and, perhaps, other Python based software you may use. The versions of different component pieces will sometimes conflict and it can be difficult to figure out why your project fails due to an update of some other package.

NOTE: As of Grok 1.2 you do not need to use virtualenv, Grok is automatically isolated from other python libraries.

In order to avoid these problems we are going to create our sample project in a "virtual environment" which will isolate it from other python libraries. Fortunately, there is an easy way to create this isolated environment using a tool called virtualenv .

Some Linux distributions provide virtualenv as a standard install for the operating system. (On Debian/Ubuntu it is called "python-virtualenv") If this is available for you, you do not need to install "easy_install" to your main Python installation, as virtualenv will include its own copy.

If this option is not available, you will need to install "easy_install" following the directions in the Grok Tutorial . There is a tutorial dedicated to explaining how virtualenv is used with Grok and how to create a project inside the environment.

Creating the Virtual Environment

Create the sample project in your home directory in a new sub-directory, called "grok". (For Windows, use "My Documents\grok". For others use "~/grok".)

Change into this new directory and create the virtual environment and our sample project. If you have difficulties or receive error messages, refer to the tutorial links above as they do provide answers to common problems.

$ cd ~/grok
$ virtualenv --no-site-packages virtualgrok
$ source virtualgrok/bin/activate
(virtualgrok)$ easy_install grokproject
(virtualgrok)$ cd virtualgrok

Preparing for the Sample Project

The grokproject application will set up your project so that it can be further developed using a tool called "zc.Buildout". This tool creates an environment that allows you to test, debug, and deploy your project code. Grokproject will even run Buildout for you the first time so that the new application is ready to run and display a simple welcome screen in a web browser.

If you have never run buildout before, it will download between 80 and 90 MB of code to create your initial Grok project. By default, it will create a cache of the source code it downloads in your home directory under ~/.buildout/eggs. This will prevent having to re-download this code every time you run buildout or grokproject.

You can change the location of this cache by creating or altering a file named ~/.buildout/default.cfg. Simply include the following lines:

[buildout]
eggs-directory = /path/to/directory

Creating the Sample Project using GrokProject

Now you are ready to run grokproject. If this is your first time running grokproject, be prepared to wait several minutes while the necessary source code is downloaded from PyPI. (Creating future projects will be much quicker because the source code will be cached locally.)

(virtualgrok)$ cd ~/grok/virtualgrok
(virtualgrok)$ grokproject music
Enter user (Name of an initial administrator user): grok
Enter passwd (Password for the initial administrator user): grok

The new sample project is located at ~/grok/virtualgrok/music and now ready for us to try out.

Note: Now that the project is created, the buildout configuration will point to the virtual Python installation within our environment. We no longer need to source the "activate" script while working with our project.

Becoming Familiar With The Project

Take a moment to become familiar with the sample project you just created by browsing through the project directory.

Directory and File Structure

Under "music" will will find several directories and files.

bin
Contains various script files to control your project. The ones that you will use most often are:
  • buildout: to rebuild your project if you make changes to its dependencies.
  • music-debug: gives you an interactive debug prompt (Python interpreter) with your project environment already configured. You can inspect and modify your stored objects and simulate browser requests.
  • paster: to run your project. Initially, it will run in its own web server process, but paster can deploy your application in many ways.
  • test: runs every unit test and functional test it can find and reports the results.
etc
Contains configuration templates for this project's installation. The template file names end with ".in". These templates are copied to the project's parts/etc directory when buildout is run. If you want to make changes to the configuration that are available the first time you run buildout or are maintained even when you reconfigure your project by running buildout again, you need to modify these files.
parts
This directory is generated the first time you run buildout. The "etc" subdirectory contains the configuration files which were generated from the configuration templates. The generated file are the one that actually effect configuration when you run the application. If you make changes to these files you must restart the application in order for them to take effect. Whenever you run buildout, any changes to these files are overwritten.
  • debug.ini and deploy.ini: configuration scripts for paster. They determine thing like what network port and address the web server will be bound to. The debug.ini will present more meaningful error messages through your browser should an error occur. Edits to this file will be lost the next time you run buildout.
  • site.zcml: contains some basic security directives for your project. The user name and password you supplied to grokproject are recorded here. (More advanced security schemes can be implemented later to avoid having clear-text passwords on the file system.) Edits to this file will be lost the next time you run buildout.
src
Contains the source code for your application and configuration information to be used if you want to deploy your code as a Python egg (music.egg-info).
src/music
Contains the source code you write.
  • app.py: the starting point for your application. You can place all your source code in this file or break it up into multiple files.
  • app.txt: a very simple functional test of the application. More tests can be added to this file or you can add tests to other text files in this directory and the test runner will find them.
src/music/app_templates
Contains html template files that grok will associate with your code in order to publish it on the web.
  • index.pt: a simple template created by grokproject that presents a welcome message. We will see this template in action when we run our project.
src/music/static
A directory where you can place static files that you want available to your application. This may include image files, cascading style sheets, or JavaScript files.
var
Contains the object database files, web server log files, and any file storage (BLOB's) maintained by the application. In a production environment, you will want to include this directory in a regular backup process as it contains all the user-entered content.

Configuration Files

There are a few configuration files you should be familiar with.

setup.py
Located in the main project directory, this file contains metadata about your project including its version and what other packages it requires to run.
buildout.cfg
Located in the main project directory, this file allows you to customize the buildout process. You can specify a project-specific location for the cached source code. You can specify other places to look for source code, or you can combine several applications into one buildout processes. Specifics about buildout can be found in the Introduction to zc.buildout tutorial.
versions.cfg
Located in the main project directory, this file initially contains a list of specific version of software that are known to work together (aka "Known Good Set" or KGS).

Run Tests and Start the Web Server

Now that we have had a look around, it is time to do something with the application we created. We will start by running the simple tests created with the project.

$ cd ~/grok/virtualgrok/music
$ ./bin/test

Running tests at level 1
Running music.FunctionalLayer tests:
  Set up music.FunctionalLayer in 1.523 seconds.
  Running:
...
  Ran 3 tests with 0 failures and 0 errors in 0.158 seconds.
Tearing down left over layers:
  Tear down music.FunctionalLayer ... not supported

The tests that were run are located in ~/grok/virtualgrok/music/src/music/tests.py. Take a look at this file, if you would like to see what was tested.

Now it is time to start the web server. By default, the web server will be available at http://localhost:8080 . If you already have a program that is using that port, you will have to assign a new port using parts/etc/debug.ini or parts/etc/deploy.ini.

Note: These changes will be lost the next time your run buildout, so if you want permanently change the default port, you will need to modify etc/debug.ini.in and/or etc/deploy.ini.in and run buildout again.
$ cd ~/grok/virtualgrok/music
$ ./bin/buildout
Note: If you want this server to be accessible from another computer you will have to modify the 'host' entry in the above files to your computer's host name, its IP address, or to 0.0.0.0 (bound to all addresses).

Run the server:

$ cd ~/grok/virtualgrok/music
$ ./bin/paster serve parts/etc/deploy.ini
Starting subprocess with file monitor
------
2009-03-06T22:12:40 WARNING root Developer mode is enabled: this is a
security risk and should NOT be enabled on production servers. Developer
mode can be turned off in etc/zope.conf
Starting server in PID 16637.
serving on http://127.0.0.1:8080

Point a web browser at http://localhost:8080 and you will be prompted for a user name and password. Enter the values that you entered when you ran grokproject. (Should be user name = grok and password = grok.)

You are now in the Grok Admin User Interface.

GrokAdminInitial.png

We will explore the Admin UI in greater detail later. For now, understand that this is a place where you can create instances of your application. Each instance will be separate and can be accessed by name as the first part of the URL following the host name.

For the Musical Performance Organizer, several bands could use the same application on the same web server by having separate instances. (There are some problems to consider in order to use that strategy and we will explore those later.) For now, this gives us a quick way to setup and delete entire instances as we are developing our application.

Create an application instance by typing "band" in the "Add Application" text box and press "Create".

You will now see the instance listed in the "Installed Applications".

GrokAdminTestApp.png

Click on the "band (Music)" link to view the instance at http://localhost:8080/band .

You should see the following text:


Congratulations!

Your Grok application is up and running. Edit music/app_templates/index.pt to change this page.


The application is up and running. Now we need to start modifying it to meet our requirements.

Initial Tests and Model Objects

Begin transcribing the application requirements into tests and perform the first coding cycle to satisfy the tests. This is the first exposure to working with model objects and unit testing.

Application Root

It's time to start implementing the application requirements. If we review the entity outline we created, we can see that our top-level object is our application root which contains "Performance" objects.

If we look at the class that was generated by grokproject (in the project directory at ~/grok/virtualgrok/music/src/music/app.py), we see that it is called "Music". This was the name we provided to grokproject with the first letter capitalized to match the naming convention for classes.

import grok
class Music(grok.Application, grok.Container):
    pass
class Index(grok.View):
    pass # see app_templates/index.pt

By looking at this class, we see that it inherits from two base classes. The first is grok.Application. This is what gets the class listed in the Grok Admin UI so that you can make instances of the application as we discussed in the previous tutorial page.

The other base class is grok.Container. This means that the class will be a dictionary-like structure that will contain key-value pairs where the values will be objects and they can be accessed using unique strings as the keys.

There are several things that this container does beyond a standard dictionary. First, it automatically persists objects placed in it to the Zope object database (ZODB). Secondly, it keeps an efficient index of its contents so that it can quickly find objects even when it contains a fairly large quantity. Lastly, it records the object hierarchy to assist code that interprets an URL in order to find a particular object in the database.

Requirements for Adding a Performance

We know from the requirements that we are going to store our performances by date. Since the key is a string, we could let the user enter any sort of date-like string for the performance and use that. The problem will be that, not only will this be hard to sort and read, but some of the strings a user might provide would require some really ugly encoding to work as part of an URL. (Remember, the URL's are supposed to understandable and a date encoded like "03%2F15%2F2009" is hard to read.)

We also know that the minimum date and time resolution for performances is an hour. A text representation of a date as "yyyy-mm-dd" with an optional "-hh" suffix will provide the resolution we need as well as provide chronological ordering by simply iterating over the container's keys.

Now, how do we enforce this rule? We could do it as part of an HTML form validation or we could try to encapsulate the container object so that performances can only be added through a method that evaluates the user input against the rule. If we do it in the form or the view code, it will have to be functionally tested as opposed to writing a unit test.

Encapsulating to force coding integrity is rather "un-Pythonic" as Python development generally assumes all developers "are adults" and will understand and respect the code they are interacting with. This "development style" generally places more emphasis on evaluating the contents of attributes and parameters when they are about to be used, rather than trying to prevent them from being assigned the wrong type of data in the first place.

There are some sophisticated ways to address this type of data validation issue, but we are going to keep it as simple as possible while still meeting our requirements. We will leave the container object "as-is" and simply add a validation method to the Music class which our view code can call before it adds a new performance to the container. This will also allow us to unit test our validation code.

Remember also, in the Phase 2 deliverables, we will be adding more sophisticated user interface controls. At that point, dates will be entered using some sort of date-picker control and it will be less likely that an invalid date will be entered by a user.

Creating Tests

We will start with some unit tests following the DocTest style. A tutorial on testing is available in the Grok documentation.

We will call our validation method "isValidKey" and modify the source code at ~/grok/virtualgrok/music/src/music/app.py to read as follows:

import grok
class Music(grok.Application, grok.Container):
    def isValidKey(self, keyVal):
        pass
class Index(grok.View):
    pass # see app_templates/index.pt

We do this so that the method exists for our tests to call. We don't want to write the code needed to satisfy our requirements yet. First we want to create some tests based on the requirements.

Create a new directory in the source code directory called "app_tests". We will use this to store all of our unit tests. (They can be located anywhere in the project source code and the test runner will find them. However, we will put them here in order to keep our project organized.)

cd ~/grok/virtualgrok/music/src/music/
mkdir app_tests
cd app_tests

Using your editor, create a file in this directory called "music.txt" with the following contents:

Tests for the Musical Performance Application.
**********************************************
:Test-Layer: unit

The Music class is a container that will hold all of the information
about "Performances".

When you create a new application instance there should be no objects
in the container::

   >>> from music.app import Music
   >>> performances = Music()
   >>> list(performances.keys())
   []


We are not going to test the pre-existing functionality of a grok.Container.
(That is already handled in its own unit tests.)
However, we do have application requirements that affect the format of
the key used to store performances in the container.

The isValidKey function needs to return "True" only when a valid string
key of the form "yyyy-mm-dd" with an optional "-hh" suffix is provided.

First we check at the basic string format::

   >>> performances.isValidKey(None)
   False
   >>> performances.isValidKey('')
   False
   >>> performances.isValidKey(0)
   False
   >>> performances.isValidKey('Not Right')
   False
   >>> performances.isValidKey('4/1/05')
   False
   >>> performances.isValidKey('4/1/2005')
   False
   >>> performances.isValidKey('2008.12.31')
   False
   >>> performances.isValidKey('2008-3-15')
   False
   >>> performances.isValidKey('08-3-5')
   False
   >>> performances.isValidKey('2009--0315')
   False
   >>> performances.isValidKey('2009-03-15')
   True

Strings that are formatted correctly should fail, if the date
is not valid::

   >>> performances.isValidKey('2009-13-01')
   False
   >>> performances.isValidKey('2009-03-32')
   False
   >>> performances.isValidKey('2009-02-29')
   False

The suffix needs to be a dash, followed by an integer from 00 to 23::

   >>> performances.isValidKey('2009-03-15Bad')
   False
   >>> performances.isValidKey('2009-03-15-')
   False
   >>> performances.isValidKey('2009-03-15-1')
   False
   >>> performances.isValidKey('2009-03-15-00')
   True
   >>> performances.isValidKey('2009-03-15-23')
   True
   >>> performances.isValidKey('2009-03-15-24')
   False

Now, we can run the application tests as we did in the previous section and see the results. Of course, everything will fail.

We can now begin implementing the isValidKey method in app.py, until all of the tests pass. There are many ways to accomplish this, so feel free to use your own implementation. You can always re-factor it later.

Here is a sample implementation:

import grok
from datetime import datetime
import time

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):
    pass # see app_templates/index.pt

If you run the application tests again, everything should pass and you should get a result that looks like the following:

Running tests at level 1
Running unit tests:
  Running:
....
  Ran 4 tests with 0 failures and 0 errors in 0.011 seconds.

Next, we will work on creating the performance object and implementing a mechanism to add them to the container.

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".

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".

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.

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.

Attaching Files to a Song

Enable users to upload sheet music and other documents related to a song in the set list.

Attachments

It is now time to add file attachments to the songs in a set list. To do this, we will need to use some components from the Zope3 component architecture that are not included in our default Grok installation.

We will be using the zope.app.file component, which you will find more fully documented in the How-to titled: Handling file uploads with zope.app.file and zope.file

Adding Components

To add new components to our application we will add them to our project's setup.py file. Edit ~/grok/virtualgrok/music/setup.py and add the line 'zope.app.file' to the "install_requires" property, so the file contents look like this:

from setuptools import setup, find_packages

version = '0.0'

setup(name='music',
      version=version,
      description="",
      long_description="""\
""",
      # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers
      classifiers=[],
      keywords="",
      author="",
      author_email="",
      url="",
      license="",
      package_dir={'': 'src'},
      packages=find_packages('src'),
      include_package_data=True,
      zip_safe=False,
      install_requires=['setuptools',
                        'grok',
                        'grokui.admin',
                        'z3c.testsetup',
                        'grokcore.startup',
                        # Add extra requirements here
                        'zope.app.file',
                        ],
      entry_points = """
      [console_scripts]
      music-debug = grokcore.startup:interactive_debug_prompt
      music-ctl = grokcore.startup:zdaemon_controller
      [paste.app_factory]
      main = grokcore.startup:application_factory
      """,
      )

We now need to run buildout again which will download the necessary software and configure it to be available in our project.

$ cd ~/grok/virtualgrok/music
$ ./bin/buildout

Adjusting the Model

We will start by editing song.py in the src directory to look like the following:

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

import zope.app.file
from zope.app.container.interfaces import INameChooser
from zope.app.container.contained import NameChooser
import urllib

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

    def __init__(self):
        super(Song, self).__init__()
        self['files'] = FileContainer()

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):
        for file in list(self.context['files'].keys()):
            del self.context['files'][file]
        self.redirect(self.url(self.context.__parent__.__parent__))
        del self.context.__parent__[self.context.__name__]

class SetList(grok.Container):
    pass

class DeleteFile(grok.View):
    grok.context(Song)
    def render(self):
        filename = urllib.unquote(self.request.get('QUERY_STRING'))
        if filename and self.context['files'].has_key(filename):
            del self.context['files'][filename]
            self.redirect(self.url(self.context))

class FileContainer(grok.Container):
    pass

class AddFile(grok.AddForm):
    grok.context(Song)
    form_fields = grok.AutoFields(zope.app.file.interfaces.IFile).select('data')

    @grok.action('Upload')
    def add(self, data):
        if len(data) > 0:
            self.upload(data)
        self.redirect(self.url(self.context))

    def upload(self, data):
        fileupload = self.request['form.data']
        if fileupload and fileupload.filename:
            contenttype = fileupload.headers.get('Content-Type')
            file_ = zope.app.file.file.File(data, contenttype)
            # use the INameChooser registered for your file upload container
            filename = INameChooser(self.context['files']).chooseName(fileupload.filename, None)
            self.context['files'][filename] = file_

class PrimitiveFilenameChangingNameChooser(grok.Adapter, NameChooser):
    grok.context(Song)
    grok.implements(INameChooser)
    grok.adapts(FileContainer)

    def chooseName(self, name):
        if name.startswith('+'):
            name = 'plus-' + name[1:]
        if name.startswith('@'):
            name = 'at-' + name[1:]

As you can see we imported several new items for use in our code. The zope.app.file will provide our storage and NameChooser will assure that our file names do not collide with one another.

Similar to the Performance object, we have added a sub-container to the Song object. Instead of being a new grok.container, it will be a new instance of FileContainer. The delete action for a song has also been modified to remove any songs. (Apparently, the contents of the FileContainer are not cleaned up as automatically as a grok.container.) Failure to remove the files before deleting the songs will result in a error like:

TypeError: There isn't enough context to get URL information.
This is probably due to a bug in setting up location information.

Notice that we have also added a new "DeleteFile" view. This time, we will not be providing the name of the file to be deleted via a form submission, but rather through a query string. This requires us to properly encode the name of the file for use in an URL and to decode it for use in our application. For this we use the Python "urllib" library.

We will generate a grok.AddForm based on the schema provided by the IFile interface. This will include a text box with a button used to browse for a file, and a second button to upload the selected file.

The upload method will use the NameChooser to fix potentially problematic file names and also to modify the filename by adding a numeric suffix when a particular filename already exists for this song.

Updating the Edit Form

We can no longer use the standard, generated edit form if we want to be able to display a list of file attachments for a song. In the src/music directory, create a new directory called "song_templates" and add the following file named "index.pt" to the directory:

<html>
<head>
  <style type="text/css">
    table { empty-cells:show; }
  </style>
</head>
<body>
<form action="." tal:attributes="action request/URL" method="post"
      class="edit-form" enctype="multipart/form-data">

  <h1 i18n:translate=""
    tal:condition="view/label"
    tal:content="view/label">Label</h1>

  <div class="form-status"
    tal:define="status view/status"
    tal:condition="status">

    <div i18n:translate="" tal:content="view/status">
      Form status summary
    </div>

    <ul class="errors" tal:condition="view/errors">
      <li tal:repeat="error view/error_views">
         <span tal:replace="structure error">Error Type</span>
      </li>
    </ul>
  </div>

  <table class="form-fields">
    <tbody>
      <tal:block repeat="widget view/widgets">
        <tr>
          <td class="label" tal:define="hint widget/hint">
            <label tal:condition="python:hint"
                   tal:attributes="for widget/name">
              <span class="required" tal:condition="widget/required"
              >*</span><span i18n:translate=""
                             tal:content="widget/label">label</span>
            </label>
            <label tal:condition="python:not hint"
                   tal:attributes="for widget/name">
              <span class="required" tal:condition="widget/required"
              >*</span><span i18n:translate=""
                             tal:content="widget/label">label</span>
            </label>
          </td>
          <td class="field">
            <div class="widget" tal:content="structure widget">
              <input type="text" />
            </div>
            <div class="error" tal:condition="widget/error">
              <span tal:replace="structure widget/error">error</span>
            </div>
          </td>
        </tr>
      </tal:block>
    </tbody>
  </table>

  <div id="actionsView">
    <span class="actionButtons" tal:condition="view/availableActions">
      <input tal:repeat="action view/actions"
             tal:replace="structure action/render"
             />
    </span>
  </div>
 </form>
  <h2>File List</h2>
  <table class="listing" border="1" >
    <thead>
      <tr><td>File URL</td><td>File Type</td><td>Remove</td></tr>
    </thead>
    <tbody>
      <tal:block repeat="file python:context['files'].keys()">
        <tr>
          <td>
            <a tal:attributes="href python:view.url(context['files'][file])"
            tal:content="python:file">Title</a>
          </td>
          <td tal:content="python:context['files'][file].contentType">Arrangement</td>
          <td>
            <a tal:attributes="href python:view.url(context) + '/deletefile?' + file">Delete</a>
          </td>
         </tr>
      </tal:block>
    </tbody>
    <tfoot>
      <tr class="controls">
        <td colspan="4" class="align-right">
          <form action="./addfile" method="post" class="edit-form" enctype="multipart/form-data">
            <table class="form-fields">
              <tbody>
                <tr>
                  <td class="label">
                    <label for="form.data">
                      <span>New Attachment: </span>
                    </label>
                  </td>
                  <td class="field">
                    <div class="widget">
                      <input class="hiddenType" id="form.data.used" name="form.data.used" type="hidden" value="" />
                      <input class="fileType" id="form.data" name="form.data" size="20" type="file"  />
                      <input type="submit" id="form.actions.upload" name="form.actions.upload" value="Upload" class="button" /></div>
                  </td>
                </tr>
              </tbody>
            </table>
          </form>
        </td>
      </tr>
    </tfoot>
  </table>
</body>
</html>

The top portion is the standard edit form, but the bottom portion will render a table that contains the attached files. The "File URL" column will contain a link to download the stored file attachment.

The "Remove" column will construct an URL which will pass the name of a given file to the "DeleteFile" view in the query string.

Notice that in the footer of the table, instead of providing a link to the add form, we actually provide the form controls required to add a file attachment and we post the result to the AddFile auto-form view.

It is important that the control names in this template match those in the auto-form. If you have problems, you can view the generated add form directly, by appending "/addfile" to the URL of song and viewing the HTML source.

The AddFile auto-form will redirect back to the song view after it adds the file. So it will appear to the user like we never left the form. Not quite as seamless as using AJAX, but it is nicer than than bouncing between two different forms when adding several objects. (We will address the use of AJAX in a later tutorial installment.)

AttachFiles.png

Updating the Performance View

To enhance the user experience further, it would be nice to allow the user to download attached files directly from the list of songs in the Performance view. Modify the set list table within the page template src/music/performance_templates/index.pt as follows:

<h2>Set List</h2>
<table class="listing" border="1" >
  <thead>
    <tr><td>#</td><td>Songs</td><td>Key</td><td>Arrangement</td><td>Files</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>
        <td>
          <tal:block repeat="file python:song['files'].keys()">
            <a tal:attributes="href python:view.url(song['files'][file])"
              tal:content="python:file">File</a><br />
          </tal:block>
        </td>
      </tr>
    </tal:block>
  </tbody>
  <tfoot>
    <tr class="controls">
      <td colspan="5" class="align-right">
        <a href="addsong">Add Song</a>
      </td>
    </tr>
  </tfoot>
</table>

We have added another repeat block in the right-most column of the table which will display a list of file attachment links within each table row. It will be unlikely to have more than one or two attachments per file.

PerformanceWithAttachments.png

Adding Comments to a Performance

We will now provide a general commenting feature for a performance.

Comments

We have almost implemented all of the Phase 1 requirements. (See entity outline ) However, we still need to provide a basic commenting function.

One approach would be to use annotations, which are a means of adding extra information to any content object without explicitly changing the object itself. All grok Model and Container objects are "annotate-able" by using grok.Annotation.

However, annotations are best used for information that will be applied to many classes of objects, but is accessed less frequently than the content of the object itself. Since, we only want to add comments to our Performance class and we will be displaying the comments every time someone views a performance, an Annotation would not be the best choice in this case.

Instead, we will follow our previous pattern of adding a new collection of content objects directly to our performances. While we will not yet be adding authentication and authorization requirements to our comments, we will be adding some extra data about the comments and who authored them to our content class.

This will pave the way for adding security and change tracking features in the future, and it will also allow us to learn a bit more about using the request object to access HTTP header information.

Interface

We will begin by adding a new interface definition. Append the following to our existing interfaces.py module:

class IComment(interface.Interface):
    """ Represents a comment involved in a distict musical performance.

    datePosted
        The date the comment was originally posted.
    whoPosted
        The person who originally posted the comment.
        If the user is not authenticated, the browser agent
        IP address is used.
    email
        The email address of the user posting the comment.
    text
        The text of the comment.
    dateModified
        The last date and time the comment was modified.
    whoModified
        The person who last modified the comment.
        If the user is not authenticated, the browser agent
        IP address is used.

    """

    datePosted = schema.TextLine(title=u"Posted", readonly=True)
    whoPosted = schema.TextLine(title=u"Posted By", readonly=True)
    email = schema.TextLine(title=u"Email", constraint=check_email)
    text = schema.Text(title=u"Comment")
    dateModified = schema.TextLine(title=u"Modified", readonly=True)
    whoModified = schema.TextLine(title=u"Modified By", readonly=True)

Notice that four of the six entries are read-only. The first two will be filled in when adding the comment. The other two will be updated any time the comment is edited. The form will only pass back user-entered data for the email address and comment text. The email address will be checked that it formatted in a valid fashion, however, no validating email will be sent to the address to confirm that the author can be contacted.

We will be using the HTTP headers to determine the IP address of unauthenticated users. For now, this will be the only type of user. While IP address is not a very sure way of identifying someone we will use it for now.

Implementation

In order to implement the new interface, we will create a file called "comment.py" in the src/music directory. Add the following to the new file:

import grok
from zope import interface
from interfaces import IComment
from datetime import datetime

class Comment(grok.Model):
    interface.implements(IComment)
    datePosted=u''
    whoPosted=u''
    email=u''
    text=u''
    dateModified=u''
    whoModified=u''

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

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

    @grok.action('Save')
    def edit(self, **data):
        self.applyData(self.context, **data)
        self.context.dateModified = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
        self.context.whoModified = self.request.getHeader('REMOTE_ADDR')
        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 Comments(grok.Container):
    pass

Notice that we have created a simple edit view for the comment, but no display view. As with our other content, we will integrate the display of comments into our Performance view.

The "self.applyData" statement will transfer the content of all the form fields to the object attributes, however, remember that the user can not edit the fields marked as "read-only". These attributes will be modified after the form data is applied. The "dataModified" attribute receives a string representing the current date and time. The "whoModified" attribute receives a string representation of the user's IP address from the request object.

The request object contains information about the several aspects of the HTTP request. (See the Grok Developer’s Notes for more details.)

We are interested in a particular environment variable from the request: REMOTE_ADDR. This contains the IP address provided to web server in the HTTP request.

In order to add new comments, we will append the following code to performance.py:

class AddComment(grok.AddForm):
    form_fields = grok.AutoFields(Comment).select('email','text')
    label = "Add Comment"

    def update(self):
        self.label = u'Add Comment for ' + self.context.__name__

    @grok.action('Add')
    def add(self, **data):
        comment = Comment()
        self.applyData(comment, **data)
        comment.datePosted = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
        comment.whoPosted = self.request.getHeader('REMOTE_ADDR')
        keyname = comment.datePosted
        if not self.context['comments'].has_key(keyname):
            self.context['comments'][keyname] = comment
            return self.redirect(self.url(self.context))

This is very similar to the edit view code, except that we are populating the other read-only attributes. Also, notice that we are storing the comments in our container based on the date and time we calculated. This will prevent duplication as long as comments are not added to a particular performance faster than once per second.

Note: This could be a concern for a site with heavy user traffic, but it is a limitation we are willing to live with for this for the practical usage of this example. We will, however, need to be aware of this limitation when doing functional testing, as the testing mechanism could easily add several comments per second.

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

import grok
from zope import interface
from interfaces import IPerformance
from musician import Musician, Musicians
from datetime import datetime
from song import Song, SetList, LongTextWidget
from comment import Comment, Comments
import time

class Performance(grok.Container):
    interface.implements(IPerformance)
    location = u''
    leader = u''
    starttime = u''
    endtime = u''
    timezone= unicode(time.strftime("%Z", time.localtime()))

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

A new "timezone" attribute was also added to the Performance class so that we can notify the user what timezone the server is using when determining timestamps on the comments. An instance of this application is unlikely to be used across wide geographic regions and we do not have enough reliable information about an anonymous user's preferred timezone settings to adjust the times correctly, so local time will be sufficient. (Hopefully, the application is hosted by a provider that is geographically close to the people using the application. Further enhancements could certainly be made to make the timezone configurable, but they will not be covered as part of this tutorial.)

Page Template Changes

We still need to update the template at src/music/performance_templates/index.pt, by appending the following within the body following the "Set List" table:

<h2>Comments</h2>
<table class="listing" border="1" >
  <thead>
    <tr><td>Date (Timezone: <span tal:content="python:context.timezone">TimeZone</span>)</td><td>Name</td><td>Comment</td></tr>
  </thead>
  <tbody>
    <tal:block repeat="key python:context['comments'].keys()">
      <tr>
        <td>
          <a tal:attributes="href python:view.url(context['comments'][key])"
          tal:content="python:context['comments'][key].datePosted">Date Posted</a>
        </td>
        <td tal:content="python:context['comments'][key].whoPosted">Who Posted</td>
        <td><pre tal:content="python:context['comments'][key].text">Comment Text</pre></td>
      </tr>
    </tal:block>
   </tbody>
  <tfoot>
    <tr class="controls">
      <td colspan="3" class="align-right">
        <form action="./addcomment" method="post" class="edit-form" enctype="multipart/form-data">
          <table class="form-fields">
            <tbody>
              <tr>
                <td class="label">
                  <label for="form.email">
                    <span>Email Address</span>
                  </label>
                </td>
                <td class="field">
                  <div class="widget">
                    <input class="textType" id="form.email" name="form.email" size="20" type="text"  />
                    <input type="submit" id="form.actions.add" name="form.actions.add" value="Add" class="button" /></div>
                </td>
              </tr>
              <tr>
                <td class="label">
                  <label for="form.text">
                    <span>Comment</span>
                  </label>
                </td>
                <td class="field">
                  <div class="widget">
                    <textarea cols="60" id="form.text" name="form.text" rows="5" ></textarea>
                </td>
              </tr>
            </tbody>
          </table>
        </form>
      </td>
    </tr>
  </tfoot>
</table>

Similar to our file attachment form, this template will list the existing comments and provide form fields and a button for submitting a new comment.

AddComment.png

The email address the user provides is not included in the comment listing as access to this information will eventually be hidden from the public.

Functional Tests

TBD

This concludes the first part of the Musical Performance Organizer tutorial.