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.

tutorials/musical_performance_organizer/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.