Teacher: Paul Carduner
Students: Brittney, Will, Preetam, and Linda
Paul:
In today's class we will be moving on to Containers, as iteration 4 suggests from the list of user stories (see http://svn.schooltool.org/trac/cando/wiki/ZContact). Specifically, it says
By logging into the ZMI, the user should be able to create a container for storing only contacts.Containers are among the most frequently used concepts in Zope3 because they allow us to create URL to object mappings in a very intuitive manner. Containers work just like python dictionaries. That is, a Container in Zope3 maps keys to values. Like with dictionaries, the values can be anything, even other Containers. If you imagined a complicated data structure represented by dictionaries of dictionaries of dictionaries and so on, then you might access an item in the the deepest dictionary like so:>>> data['key1']['key2']['key3'] <some value>This easily maps to and from a url that would look like this:http://dataserver/key1/key2/key3. This of course also looks like directories in your filesystem. So in terms of data storage, especially when it comes to storing objects, it is really intuitive to store data in containers that act just like directories on your file system.But before we jump right into implementing containers, we will write tests first. This is where test-driven development really starts for us. Writing tests will help us outline how we want our container to work. So go ahead and open up our unit testing file:
README.txtNow before we start adding stuff about containers, lets just do a few more things with the Contact object that will help us out later when we are working with containers. So add the following to the end of the
README.txtfile.We can of course set these attributes too. >>> paul.firstName = u'Paul' >>> paul.lastName = u'Carduner' >>> paul.firstName u'Paul' >>> paul.lastName u'Carduner' To help programmers, there is also a programmer representation of a Contact instance that shows the last and first name. >>> paul <Contact "Carduner, Paul">When we do stuff with containers, we are going to want to have at least one contact ready to use in a container. It is pretty boring to use a contact with none of its attributes set, so we set some of paul's attributes. Secondly, we want to get something intelligible back when we just print out the programmer representation of an instance. By default python gives us<zcontact.contact.Contact instance at 0xb745826c>, which won't help at all.If you run the tests again, they should fail. So now open up
contact.pyand add the following method to the Contact class:def __repr__(self): """Returns programmer representation of Contact.""" return '<%s "%s, %s">' % (self.__class__.__name__, self.lastName, self.firstName)The tests should now pass.Onward with Containers! Go back to the
README.txtfile and add these lines, which are going to look a lot like the start of the tests for creating Contacts.Creating Contact Containers --------------------------- We can also create containers that store multiple contacts. This helps for organization of contacts within the system. >>> from zcontact.contact import ContactContainer >>> container = ContactContainer() >>> interfaces.IContactContainer.providedBy(container) True The containers has only one basic attribute: a title. >>> container.title u'' >>> container.title = u'Cool People' >>> container.title u'Cool People'As I said earlier, Containers are frequently used in Zope3. This means that Zope3 already defines some useful interfaces and implementations for container objects. We want to harness what Zope already does for us, so we're going to want the IContactContainer interface to extend a Zope defined interface, specifically
zope.app.container.interfaces.IContainer. So we will add a check for that interface and write the rest of our tests.The ``IContactContainer`` interface also inherits from zope's ``IContainer`` interface so we have all the functionality outlined in that interface. >>> from zope.app.container.interfaces import IContainer >>> interfaces.IContactContainer.isOrExtends(IContainer) True We can add contacts to the contact container just like adding an item to a python dictionary. Contact lookups work the same way. >>> container['paul'] = paul >>> container['paul'] <Contact "Carduner, Paul"> Now let's add another contact to show off some other features. >>> container['eldar'] = Contact() We can delete contacts. >>> del container['eldar'] Basically, the ContactContainer implements all the functionality present in python dictionaries. The ``ContactContainer`` class also has a nice programmer representation that shows the title of the container and the number of contacts in the container. >>> container <ContactContainer "Cool People", 1 contact>Now that we have finished writing the tests for the code we are about to write, lets get to writing the actual code! We will go through this and make each test pass one by one. The first test asks for the existence of the ContactContainer class and the IContactContainer interface, so let's create those. In interfaces.py add the following:
class IContactContainer(zope.interface.Interface): """The interface for a contact container."""Remember that we are only writing code to make each of the tests pass one by one. We'll do it this way for now so you get the idea of the process of making tests pass. In the future we will be developing code faster and with less attention to detail. Next add the following to contact.py:class ContactContainer(object): """Implementation of IContactContainer.""" zope.interface.implements(interfaces.IContactContainer)The tests should still fail, but now the output isFailed example: container.title Exception raised: Traceback (most recent call last): File "/home/pcardune/Zope3/src/zope/testing/doctest.py", line 1361, in __run compileflags, 1) in test.globs File "<doctest README.txt[18]>", line 1, in ? container.title AttributeError: 'ContactContainer' object has no attribute 'title'So we know to move on to adding the various attributes. In fact, let's go ahead and finish the entire interface. Go back tointerfaces.pyand make the IContactContainer interface look like this:class IContactContainer(zope.app.container.interfaces.IContainer): """The interface for a contact container.""" title = zope.schema.TextLine( title=u"Title", description=u"Title for the container of Contacts.", required=True)Don't forget toimport zope.app.container.interfacesat the top of the file. Now when we jump back to contact.py and try to implement this interface, we might notice a problem: what does IContainer require of its implementations? Well, one way to find out is to actually open up the file that defines the IContainer interface, which on maddog is located here: /usr/local/src/Zope3/src/zope/app/container/interfaces.py. Fortunately though, there is already an implementation of this interface in zope! So to implement the IContactContainer interface (which extends the IContainer interface), out implementation just has to extend an implementation of IContainer. The implementation of IContainer I happen to be talking about is zope.app.container.btree.BTreeContainer. There are other implementations of IContainer that we could use, but BTreeContainer is the one most often used. TheBstands forBinary. That said, now make your ContactContainer class look like this:class ContactContainer(zope.app.container.btree.BTreeContainer): """Implementation of IContactContainer.""" zope.interface.implements(interfaces.IContactContainer) title = u''Again, don't forget toimport zope.app.container.btreeat the top of the file. After running the tests again, you will notice that a lot of the later tests involving adding and deleting contacts from the container already pass, as they are covered by the BTreeContainer class. The last test to make pass is the one about programmer representation. So add the following method to the ContactContainer class:def __repr__(self): """Returns programmer representation of ContactContainer.""" return '<%s "%s", %s contact>' % (self.__class__.__name__, self.title, len(self))Now all the tests should pass.Now that we have finished with the data representation part of the container user stories, we need to begin working on the other user stories that involve how the containers are viewed in the ZMI. The first thing we want to be able to do is create the container object through the ZMI. Naturally, we will write functional tests that describes how this is done. So open up
browser.txtand add the following code for creating a Contact Container through the ZMI.Creating a Contact Container ---------------------------- Creating a contact container through the ZMI works just like creating a Contact. >>> browser.open('http://localhost/manage') Under the add menu, we should see a link for Z Contact Container. Clicking on it takes us to a form where we can enter information for a new contact. >>> browser.getLink('Z Contact Container').click() >>> browser.getControl('Title').value="Cool People" >>> browser.getControl(name='add_input_name').value="coolpeople" >>> browser.getControl('Add').click()Before we go on to implement the browser views, we have to set up permissions on the ContactContainer class. You may remember from implementing the Contact class, that we had to edit theconfigure.zcmlfile. Open up that file, and we are going to want to add the following class registration:<class class="zcontact.contact.ContactContainer"> <require permission="zope.View" interface="zcontact.interfaces.IContactContainer" /> <require permission="zope.ManageContent" set_schema="zcontact.interfaces.IContactContainer" /> </class>But to make this test pass we have to implement an addform for the ContactContainer object. We will do this inconfigure.zcmljust like we did for the Contact object. So add the following toconfigure.zcml.<browser:addMenuItem title="Z Contact Container" description="Add a Z Contact Container" class="zcontact.contact.ContactContainer" view="addContactContainer.html" permission="zope.ManageContent" /> <browser:addform label="Add Contact Container" name="addContactContainer.html" permission="zope.ManageContent" schema="zcontact.interfaces.IContactContainer" content_factory="zcontact.contact.ContactContainer" />Go ahead and run the tests again just to make sure everything worked. Now we will get back to writing more functional tests. So editbrowser.txtto include the following tests:Now that the Contact Container has been added, we should be able to click on a link to it from the ZMI. We want this to take us to a page that displays a list of contacts in the container. Of course, after we have created the contact container, there are initially no contacts in it. So there will be a link or button that takes you to a screen for adding a contact. >>> browser.getLink('coolpeople').click() >>> browser.getLink('Z Contact').click() >>> print browser.contents <BLANKLINE> ...Add Contact... >>> browser.getControl('Last Name').value="Carduner" >>> browser.getControl('First Name').value="Paul" >>> browser.getControl(name='add_input_name').value="paulcarduner" >>> browser.getControl('Add').click() >>> print browser.contents <BLANKLINE> ...paulcarduner... >>> browser.getLink('paulcarduner').click() >>> print browser.contents <BLANKLINE> ...Z Contact... ...Name:... ...Carduner, Paul...Now, if we follow the pattern that we used in writing the Contact object's views, the first thing we would do is write a page template that displays the information we are asking for in the functional test. However, containers can work a bit differently because they are so heavily used that Zope already has default views available for them. However, we need to set permissions on these views so they are available to us. Later we will want to customize these views and will most likely rewrite them, but for now we will just use zope's defaults. So open upconfigure.zcmland add the following registration:<browser:containerViews for="zcontact.interfaces.IContactContainer" contents="zope.ManageContent" add="zope.ManageContent" />This tag registers two views for theIContactContainerinterface: the contents view and the add view. The contents view is exactly what you see when you first log in to the ZMI: a view that shows you the contents of the root folder in your instance. So here we say that people with the zope.ManageContent permission are allowed to see that view. The add view gives us the add menu on the left that allows us to add objects to the container. You also see the add view when you first log in to the ZMI.If you restart your zope server now and log in, you can add a contact container and even add a contact to the container. But you will notice when you are in the container view that we can add absolutely anything to it. Usually when you create a container, it is for storing very specific types of objects. We only want the user to be able to add a Z Contact object. To do this, we have to put a constraint on the container interface. So open up the
interfaces.pyfile and make the IContactContainer interface look like the following:class IContactContainer(zope.app.container.interfaces.IContainer): """The interface for a contact container.""" title = zope.schema.TextLine( title=u"Title", description=u"Title for the container of Contacts.", required=True) zope.app.container.constraints.contains(IContact)Don't forget toimport zope.app.container.constraintsat the top of the file.
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
A tarball of all these files is located here: lesson05.tgz