Teacher: Paul Carduner
Students: Brittney, Will, Preetam, and Linda
Paul:
Two weeks ago we didn't finish the containers and they still don't do much. The purpose of making our own containers is to put them to work for us. We want containers to help us manage contacts in some significant way.
So, what do we want our container to do? Before we discuss that further, what does our container do already?
Brittney:
It holds contacts and the contact information.
Paul:
Right, Brittney. In the last class we managed to create a container where we could add contacts. But now we want to make this container work for us. That is, we want it to help us manage our contacts in some way. The first thing we want to do is have our container display the contacts inside it how we want them to be displayed.
As always, we are going to write tests first. Since we are changing the way things are going to look, we will modify the functional tests. I also recommend creating an additional contact in the functional tests so that you have something to play with. So open up
browser.txtand add the following.We will go ahead and add some more contacts to play with. >>> browser.open('http://localhost/coolpeople') >>> browser.getLink('Z Contact').click() >>> browser.getControl('Last Name').value="Omuraliev" >>> browser.getControl('First Name').value="Eldar" >>> browser.getControl(name='add_input_name').value="eldaromuraliev" >>> browser.getControl('Add').click() >>> browser.getLink('Z Contact').click() >>> browser.getControl('Last Name').value="Elkner" >>> browser.getControl('First Name').value="Jeff" >>> browser.getControl(name='add_input_name').value="jeffelkner" >>> browser.getControl('Add').click() If we go to the contact container directly, then we just get a simple list of the contacts. >>> browser.open('http://localhost/coolpeople') >>> print browser.contents <BLANKLINE> ...Omuraliev, Eldar... ...Elkner, Jeff... ...Carduner, Paul...After writing your test, you should run them just to make sure they are actually failing (remember we write failing tests, then write code to make them pass).
Will:
A question about the test: When we display the contents of our container, how do we determine the order in which the names will be displayed?
Paul:
The order is alphabetical by the name of the contact object. You can always change the way the test later if the test failure is caused by a poorly written test (for example, one with the wrong ordering).
Since that is now done, we need to implement the changes. But remember, we want to run these tests first just to make sure this isn't already done for us somehow. We will have to create a new page for the index. Remember that pages are registered in zcml so let's open up
configure.zcml. We will simply add the following configuration tag:<browser:page for="zcontact.interfaces.IContactContainer" name="index.html" permission="zope.View" template="contactcontainer.pt" menu="zmi_views" title="View" />Since we specified the contactcontainer.pt file as the template for this page, we have to create that page template. I like to to create page templates by first copying an existing one and then editing it. This way I don't have to remember all the names of the macros I am looking for. So I would copy
viewcontact.pttocontactcontainer.ptand then editcontactcontainer.ptto look like this:<html metal:use-macro="context/@@standard_macros/view"> <body metal:fill-slot="body"> <h1>Z Contacts: <span tal:replace="context/title">Container Title</span></h1> <div tal:repeat="contact context/values"> <a tal:attributes="href contact/@@absolute_url" tal:content="string:${contact/lastName}, ${contact/firstName}" > Contact's Name </a> </div> </body> </html>We want a
tal:repeatto repeat over the contents of the container. And remember that since this view is registered for the container, the context variable will be the container. We can grab the contents of the container using thevaluesmethod. It has the same API as a python dictionary. So when we write<div tal:repeat="contact context/values">, it would look like the following in Python:for contact in context.values(). Then the div will be created multiple times, once for each contact and any tal attributes inside the repeat has access to the "contact" variable, just like in python for loops.Note that inside that div we have a link and the link has dynamically generated
hrefattributes and a dynamically generated content. When we writetal:attributes="href contact/@@absolute_url"it sets thehrefattribute to the url of the contact object.The
@@part says that this is a view lookup. Remember that pages are views and that we can access all of our pages with the @@ in front of the name of the page (like@@index.html).@@absolute_urlis a special view that Zope has which returns just a plain string with the url of the object. In fact, you can go to any object in your ZMI and just add@@absolute_urlto the end of the url and see what you get. So start up your zope servers and try it out.Anyway, now the functional tests should pass.
So what's next? Well the user stories say that the contacts should be ordered alphabetically by last name. Right now they are ordered based on the Object Name we specified when adding the objects (i.e. paulcarduner and jeffelkner). Alphabetical ordering constitutes a bit more logic than we can handle in a page template so we will have to create a view class for the container. Remember the view class we had for editing a contact? Well we will do something like that again, except this time it won't get deleted.
The first thing we should do is modify our test so that the names are actually displayed in order. The end of my
browser.txtfile now looks like this:>>> browser.open('http://localhost/coolpeople') >>> print browser.contents <BLANKLINE> ...Carduner, Paul... ...Elkner, Jeff... ...Omuraliev, Eldar...Next we have to tell zope that our page is going to use a browser view class. You may remember from before that this is done by adding the
classattribute to the page registration in configure.zcml. It should look like this:<browser:page for="zcontact.interfaces.IContactContainer" name="index.html" permission="zope.View" template="contactcontainer.pt" class="zcontact.browser.ContactContainerView" menu="zmi_views" title="View" />Now we need to create the
ContactContainerViewclass in a file calledbrowser.py. So as we create this class, let's think about what a browser view does again. Remember that a browser view takes an object (the context of the page we are displaying) and a request (the HTTP request from the users web browser) and does stuff with them. Also, the browser view class can be accessed from the page template through theviewvariable.A lot of the time, our view class only has to have a few methods that help the template do extra hard python processing. In this case, we should only have one method in our class, called
getSortedContacts. Here I am going to throw some more advanced python at you because we are going to use a lambda function for sorting. So you have something to look at, here is what I have written for my view class:class ContactContainerView(object): """View for showing a list of contacts""" def getSortedContacts(self): """Return the list of contacts, but sorted by last name.""" return sorted(self.context.values(), cmp=lambda a,b: cmp(a.lastName, b.lastName))The
sortedfunction is a standard python function that takes a list of items and returns another list with those items sorted. So if there were a list of numbers it would sort them from smallest to largest, and if there were a list of strings, it would sort them alphabetically. But we have neither of these. Rather, we have a list of objects. Python doesn't know how to sort objects so we have to specify some function for comparing the objects - for telling python which objectgreaterthat the other.Since we are ordering the contacts based on last name, it makes perfect sense to compare the objects by actually comparing their
lastNameattributes.Rather than right a whole separate method just for comparing the objects, we will write a lambda function. Lambdas are a great way to write one line functions. Expanded, the lambda we have here would look like:
def someFunc(a, b): return cmp(a.lastName, b.lastName)The syntax for lambda functions is simply
lambdathen the parameters the function takes, and after the colon is what the function returns. So this just returns a comparison of thelastNameattribute as we desired.With the browser view class written, we need to make our page template utilize the view class. Instead of repeating over
context/values, we will repeat overview/getSortedContacts. The change looks like this:<div tal:repeat="contact view/getSortedContacts"> <a tal:attributes="href contact/@@absolute_url" tal:content="string:${contact/lastName}, ${contact/firstName}" > Contact's Name </a> </div>With that our tests should pass.
The user stories also say that we should be able to delete contacts from this view. We will have to add this functionality ourselves to the browser view class and the page template.
But first, let's write functional tests for this functionality. I think a good way to add delete functionality to our user interface is using checkboxes - the way zope does it already. There should be one checkbox for each contact, and a delete button at the bottom that will delete all the checked contacts. As a functional test, this description would look like this:
We can also delete contacts be clicking on the checkboxes next to their names and hitting the delete button. >>> browser.getControl(name="paulcarduner.delete").value = True >>> browser.getControl("Delete").click() >>> "Carduner, Paul" not in browser.contents TrueThis time we will explicitly tell the browser which checkbox to check by specifying the name of the checkbox input element (rather than the name of a label associated with it).
To solve this problem we have to think about it a bit more carefully. Specifically, how are we going to pass on which contact we want to delete to the browser view class. Remembering what the browser view class takes as parameters, it's clear that the request object is a good place to store this information. We can store things in the request object using forms. So now lets go back to
contactcontainer.ptand add in the checkboxes.In the body of my page template, I have encapsulated the repeat in a form like so:
<form tal:attributes="action request/URL" method="POST"> <div tal:repeat="contact view/getSortedContacts"> <input type="checkbox" tal:attributes="name string:${contact/__name__}.delete" /> <a tal:attributes="href contact/@@absolute_url" tal:content="string:${contact/lastName}, ${contact/firstName}" > Contact's Name </a> </div> <input type="submit" value="Delete" name="DELETE_SUBMIT"/> </form>The request object has a variable, URL which is just the URL for the page you are looking at (kind of like @@absolute_url except that it will give you the page too, as in @@index.html). Then you should add a check box before the link inside the repeat:
<input type="checkbox" tal:attributes="name string:${contact/__name__}.delete" />Again, the name attribute for the checkbox is going to be based on the name of the contact object. We know that these names will be unique because all objects in a container have unique names and we access the name of an object through the
__name__attribute. Then we can append.deleteto that name so we know the checkbox is for deleting and now something else. Finally we should put the delete button at the bottom of the form:<input type="submit" value="Delete" name="DELETE_SUBMIT"/>Now upon clicking the Delete button, our browser view should be sent the selected checkboxes through the request object. Based on this fact, we need to implement deletion of the objects in the browser view. Since we want this to happen right when the page is loaded, we will put the functionality in the __init__ method. My
ContactContainerViewclass now looks like this:def __init__(self, context, request): self.context = context self.request = request if 'DELETE_SUBMIT' in self.request: for key in self.request: if key.endswith('.delete'): objName = key[:-7] del self.context[objName]With that the tests should pass.
I don't know if I have assigned this before, but there is going to be homework for next week. The homework will be to read over the zope page template reference which can be found at http://www.zope.org/Documentation/Books/ZopeBook/2_6Edition/AppendixC.stx.
I would also like to introduce you to bzr. Kevin Cole made a good bzr tutorial here http://dc.ubuntu-us.org/resources/tutorials/bzr-intro.php. You should read over it. bzr is a revision control system that allows you to manage changes in a project. We will be using it extensively over the summer. I want you to make your zcontact application into a bzr repository. Also try adding some little bit of cool functionality to the existing application. Next week we will have a small show and tell of what people did. See you all next week!
Files:
When you click on the links belove you will be presented with text versions of these files. If you choose to save these files from your web browser, be sure to put the files in the proper directory structure.
- zcontact/interfaces.py
- zcontact/contact.py
- zcontact/zcontact-configure.zcml
- zcontact/configure.zcml
- zcontact/__init__.py
- zcontact/viewcontact.pt
- zcontact/tests.py
- zcontact/ftests.py
- zcontact/README.txt
- zcontact/browser.txt
- zcontact/browser.txt
A tarball of all these files is located here: lesson06.tgz