We showed you: ✦ How to instantiate and edit AddressBooks ✦ How to add instances of the Entry class to the AddressBook ✦ How to list entries in the AddressBook ✦ How to delete entries fr
Trang 1Making the changes to entryDetails.dtmladds an Edit Entry button at the top ofthe page that makes it easier to access the editEntryForm.
After changing the file, reload the page for an entry (such as /Test/1/) and you’ll see the button at the top of the page Click it and you’ll be taken to the
editEntryFormmethod for that entry
Congratulations! You’ve created a basic AddressBook application product It isadmittedly a little rough around the edges, but it has all of the basic functionalitythat such an application requires In the next chapter, we show you how to addsome polish and ease of use
The code for the product at this point can be found in the /chapter_07/ Addressit_4directory on the accompanying CD-ROM
Summary
In this chapter we showed you step-by-step how to create a basic Web application
as a Zope Python Product We showed you:
✦ How to instantiate and edit AddressBooks
✦ How to add instances of the Entry class to the AddressBook
✦ How to list entries in the AddressBook
✦ How to delete entries from the AddressBook
✦ How to traverse the AddressBook into the entry instances
✦ How to edit the entry instances
In the next chapter, we show you how to increase the sophistication of theAddressBook’s user interface
On the CD-ROM
Trang 2Enhancing the AddressBook
In Chapter 7 we demonstrated the design and
implementa-tion of a fairly straightforward AddressBook applicaimplementa-tion Inthis chapter, we show you how to continue and improve theAddress Book’s functionality and user interface
Adding a Standard Header
Until now, none of the DTML files that we’ve added to theAddressBook application have shared any code or layoutbesides the standard headers and footers Zope already uses
In order to make it easier to create standardized navigationfor the application, a new header for the AddressBook is agood idea
Add a StandardAddressBookHeader.dtmlfile to the
/Addressit/dtml/directory with the following code:
<table width=”100%” cellspacing=”0”
Batching the Entriesdisplay
Grouping EntriesAdding and deletinggroups
Retrieving Entries bygroup
Renaming groupsSorting Entries bycolumn
Trang 3Then edit indexAddressBook.dtmlso that the beginning of the file reads:
<dtml-var standard_html_header>
<dtml-var standard_addressbook_header>
<form action=”.” method=”post”>
And edit the entryDetails.dtmlfile so that the beginning of the file reads:
<dtml-var standard_html_header>
<dtml-var standard_addressbook_header>
<form action=”./editEntryForm” method=”post”>
<input type=”submit” value=”Edit Entry”>
</form>
<table cellspacing=”0” cellpadding=”3” border=”0”>
Notice that both DTML files have had <dtml-var standard_addressbook_header>
added to them, but both have also had some code removed From theAddressBook’s index_htmlmethod (indexAddressBook.dtml) we’ve removed the
<dtml-var title>, and from the Entries index_html(entryDetails.dtml) we’veremoved the code that renders the first and last names of the Entry
Now, the new standard_addressbook_headermethod will obviously replace theAddressBook’s <dtml-var title>by rendering <dtml-var title_or_id>, but theEntry class currently has no title attribute or method, and you’ve just removed thecode that renders the FirstName and LastName attributes What you need to donow, is create a titlemethod that returns an appropriately formatted name Addthe following method to the Entry class in Entry.pyafter the editEntrymethod andbefore the Web methods section:
return self.FirstName + “ “ + self.LastName
This titlemethod returns a string consisting of the FirstName, MiddleInitial, and
LastNameattributes (with appropriate punctuation and spacing) if the MiddleInitial
attribute is not blank, but the titlemethod returns a string consisting of the
FirstNameand LastNameattributes only (separated by a space) if the MiddleInitial
attribute is blank The standard_addressbook_headerwill now render the Entryclass’
titlemethod when it tries to render title_or_id
Trang 4Batching the Entries Display
The AddressBook application currently lists all of the contained Entries on a singlepage, regardless of how many entries there are This is done by calling the
listEntriesmethod from the AddressBook’s index_htmlDTMLFile
The listEntriesmethod is fairly simple It just returns the dictionary’s values()
method, which returns a list that contains the objects in the dictionary:
def listEntries(self):
“Method to list Entries”
return self.Entries.values()Note that if the listEntriesmethod had instead returned self.Entries, the resultwould have been a list of key/value pairs, rather than a list of the values alone,which would have made accessing the attributes of the values (which are Entryinstances) more difficult, though not impossible
The code in indexAddressBook.dtml(which is mapped to index_htmlby the class)calls the listEntriesmethod like this:
<dtml-in listEntries>
<input type=”checkbox” name=”EntryIDs:list” value=”&dtml-id;”>
<a href=”./&dtml-id;”><dtml-var FirstName> <dtml-varLastName></a><br>
</dtml-in>
which renders a checkbox and the first and last names for each returned object,linking the names to the actual Entry, which displays the Entry’s index_html.However, now that the Entry class has a title method, you can simplify this byreplacing the DTML that renders the first and last names like this:
<dtml-var standard_html_header>
<dtml-var standard_addressbook_header>
<form action=”.” method=”post”>
<input type=”submit” name=”delEntries:method” value=”Delete”>
<input type=”submit” name=”addEntryForm:method”
value=”Add”><br>
<dtml-in listEntries>
<input type=”checkbox” name=”EntryIDs:list” value=”&dtml-id;”>
<a href=”./&dtml-id;”><dtml-var title></a><br>
</dtml-in>
<dtml-var standard_html_footer>
which simplifies the DTML somewhat, and results in a nicer formatted name
Trang 5Scaling to many results
But what you should be concerned about here is that the page displays all of theEntries at once When the AddressBook contains many Entries, this will result intwo things:
✦ The page will be much longer, forcing users to scroll farther to find the entrythey want
✦ The page will take longer to download and render, forcing users to wait.This clearly has a negative impact on the usability of the AddressBook Fortunately,there is a fairly simple solution Zope has optional attributes for the <dtml-in>tagthat enable you to specify the beginning of the sequence you are iterating over, aswell as how large the sequence is Taking advantage of these optional attributes isfairly simple
Rewrite the indexAddressBook.dtmlfile so that the <dtml-in>tag reads as follows:
<dtml-in listEntries size=20 start=start sort=LastName,FirstName,MiddleInitial>
You’re doing three things here First, you’re telling the <dtml-in>tag to display 20results at a time Second, you’re telling the tag to start at the sequence item whosenumber is contained in the “start” variable Obviously the start variable needs tocome from somewhere, so add the following to the top of the indexAddressBook dtmlfile, just below the two header variables:
The third thing you’re telling the <dtml-in>tag to do is to sort the result set by theLastName, FirstName, and MiddleInitial attributes Using a compound sort meansthat the list is sorted first according to the LastName attribute, and in case of a tie,
by the FirstName attribute If the result is still tied, then the MiddleInitial attribute
is the final tiebreaker Sorting according to this format is standard office filingpractice, and should be familiar to your users
About orphans
If you go ahead and test the AddressBook application at this point and begin addingmore Entries, you’ll notice something curious as you get to the twenty-first entry,namely, that it’s being listed on the page (See Figure 8-1.) What’s going on here andwhy isn’t the batch being limited to 20 items like we asked?
Trang 6It turns out that the <dtml-in>tag has an optional attribute called orphan Orphans
are “dangling” sequence items that you might want to aggregate with another batch(presumably they’d be lonely by themselves) The default value for the orphanattribute (meaning unless you set it explicitly yourself) is three, which means that ifthe next batch is smaller than three, its results are appended to the current batch
so that they don’t dangle
If you want to disable the orphan functionality, all you need to do is set the orphanattribute to zero, like this: <dtml-in listEntries orphan=0> However, you don’treally need to do that for this AddressBook application
Figure 8-1: Orphans are added to the previous batch
Navigating among the batches
Okay, so how can the user get to the next batch? You could ask them to rewrite theURL to pass in a new start value, like so:
http://128.0.0.1/Test?start=21This would work, as we can see in Figure 8-2, but it’s unreasonable to expect users(even technically inclined users) to navigate by rewriting URLs in their browser’saddress field
Trang 7Figure 8-2: Manually displaying a different starting point
However, you can certainly automate this process by creating links on the page thatpass in this value for the user in an easier to use way
There’s more than one way to create this sort of batch navigation, such as creatingprevious/next links that look like this:
< Previous Next >
But we’ve found that generally speaking, a navigation interface should inform theuser as to how much information is present and enable the user to skip to anarbitrary location within the set
So, we’ll show you how to build a general batch navigation interface that looks likethis:
[1-20] [21-40] [41-53]
which will automatically expand to fit the number of results available
First, edit indexAddressBook.dtmlat the beginning of the file to match the followingcode:
<dtml-var standard_html_header>
<dtml-var standard_addressbook_header>
Trang 8<dtml-unless start>
<dtml-call “REQUEST.set(‘start’, 1)”>
</dtml-unless>
<dtml-in listEntries previous size=20 start=start>
<dtml-in previous-batches mapping>
[<a href=”&dtml-absolute_url;?start= &dtml-batch-start- Ænumber;”><dtml-var batch-start-number> - Æ
<dtml-in listEntries next size=20 start=start>
<dtml-in next-batches mapping>
[<a href=”&dtml-absolute_url;?start= &dtml-batch-start- Ænumber;”><dtml-var batch-start-number> - Æ
method between the <dtml-unless>block and the beginning of the form, each ofwhich iterates over the same listEntriesmethod that the main display does,further along the file
The three new sections do the following:
✦ Render links to the batches before the current one
✦ Render an indicator of the current batch
✦ Render links to the batches after the current oneLet’s take a closer look at each new section you’ve just added
Trang 9First, we’ve got the following code:
<dtml-in listEntries previous size=20 start=start>
<dtml-in previous-batches mapping>
[<a href=”&dtml-absolute_url;?start= &dtml-batch-start- Ænumber;”><dtml-var batch-start-number> - Æ
<dtml-var batch-end-number></a>]
</dtml-in>
</dtml-in>
You can see that the code starts off by seemingly iterating over the current batch,
as defined by the “start” attribute, except that we are also adding a “previous”attribute The previous attribute causes the in tag to only iterate once if there areany previous batches, and sets various special batch variables to the previoussequence The batch variable that you’re interested in here is “previous-batches,”
which, when iterated over using the mapping keyword, creates a sequence of
mapping objects that make “start-batch-number,” “batch-end-number,” and
size” available as variables You can see that the code here uses the start-” and “batch-end-” variables to construct a link that shows the range of thebatch being pointed to, and passes the “batch-start-number” value in the URL asthe value for “start.”
“batch-The next section is a bit simpler:
<dtml-in listEntries size=20 start=start>
The second bit of code here does the same thing except that it tests whether thecode is executing on the last item of the sequence and if so, renders the sequenceindex plus one
And that’s it for the current batch — it doesn’t need to be linked to, it just musthave a placeholder displayed
Trang 11Figure 8-4: Batch Navigation 2
The code for the product at this point can be found in the /chapter_08/ Addressit_5directory on the accompanying CD-ROM
Grouping Entries
The changes you’ve made to the AddressBook in this chapter make it easier tomanage and navigate Entries once you have more than 30 Entries Now theAddressBook is capable of displaying any number of Entries by batching throughthem, so you can easily manage hundreds of Entries However, suppose that you
need to navigate among thousands of Entries, or that you need to categorize the entries in some way If so, you should consider another enhancement: grouping.
There are two basic approaches for adding grouping to the AddressBook:
✦ Groups could be objects contained within the Addressbook, and containingEntries themselves
✦ A Group could be a simple attribute of an Entry
Both approaches have merit In general, unless the Group has to have someattributes of its own beyond a simple label, the second approach is simpler In thiscase, a group is simply a labeled category, so we’ll go with the second approach
On the CD-ROM
Trang 12Adding a GroupList attribute to the AddressBook class
Because the list of Groups that Entries can be categorized into is common to allEntries, a GroupList attribute should belong to the AddressBook class Thisattribute can be a simple list of strings
Change the AddressBook class’ init method to read as follows:
def init (self, id, title):
self.id = idself.title = titleself.ContactTypeList = [‘Email’, ‘Home’, ‘Mobile’,
‘Work’, ‘Pager’, ‘ICQ/IM’,
‘URL’, ‘Extension’]
self.Entries = {}
self.LastEntryID = 0self.GroupList = [‘Unfiled’, ‘test1’, ‘test2’]
The AddressBook class also needs a method similar to the listContactTypes
method that will return the list of Groups, so add the following code to theAddressBook class (in AddressBook.py) just before the section labeled #Methods to add, edit, delete and retrieve Entries:
# Methods to manipulate groupsdef listGroups(self):
“Returns a list of Groups”
return self.GroupListMore methods to manipulate the list of Groups will be created in a later section ofthis chapter
Adding a Group attribute to the Entry class
In the Entry class, most of the attributes are initialized in the editEntrymethod, soyou need to edit this method to take an additional optional parameter, Group, withits default value as “Unfiled.” In addition, in the body of the method, the Groupattribute must be set as well Edit the Entry.pyfile so the editEntrymethodcorresponds to the following code:
def editEntry (self, FirstName, MiddleInitial, LastName,
Address1 = “”,Address2 = “”,City = “”,State = “”,ZipCode = “”,Country = “”,Company = “”,Title = “”,
Trang 13Group = “Unfiled”,Contact_1 = “”, ContactType_1 = “”,Contact_2 = “”, ContactType_2 = “”,Contact_3 = “”, ContactType_3 = “”,Contact_4 = “”, ContactType_4 = “”,Contact_5 = “”, ContactType_5 = “”,Contact_6 = “”, ContactType_6 = “”,REQUEST = None
):
“Method for updating Entries”
self.FirstName = FirstNameself.LastName = LastNameself.MiddleInitial = MiddleInitialself.Address1 = Address1
self.Address2 = Address2self.City = City
self.State = Stateself.ZipCode = ZipCodeself.Country = Countryself.Company = Companyself.Title = Titleself.Group = Groupself.Contact_1 = Contact_1self.ContactType_1 = ContactType_1self.Contact_2 = Contact_2
self.ContactType_2 = ContactType_2self.Contact_3 = Contact_3
self.ContactType_3 = ContactType_3self.Contact_4 = Contact_4
self.ContactType_4 = ContactType_4self.Contact_5 = Contact_5
self.ContactType_5 = ContactType_5self.Contact_6 = Contact_6
self.ContactType_6 = ContactType_6
if REQUEST is not None:
return self.editEntryForm(self, REQUEST) After you’ve made this change to the Entry.pyfile, a corresponding change needs
to be made to the editEntryForm.dtmlfile, so as to provide a way to set the Groupfor the Entry Edit editEntryForm.dtmlso as to add the following between the rowsfor the “Company” form element and the “Contact_1” form element
Trang 14be selected in the form element.
At this point, you’ve made all the changes necessary to set a Group attribute on anEntry Unfortunately, any AddressBooks that you have already created don’t have a
GroupListattribute, so refreshing the Product won’t be very much help You’ll have
to delete the existing AddressBook and create a new one after refreshing Afteryou’ve added an Entry, check its editing view and you should see something likethe screen shown in Figure 8-5
Figure 8-5: Editing an Entry with a Group attribute
Choosing another Group (such as test1) and submitting the change will demonstratethat the Group attribute is being persisted correctly in the ZODB (Zope ObjectDatabase)
Trang 15Adding and Deleting Groups
In the previous section, you added the ability to set a Group attribute on an Entryobject This works in a substantially similar way to the various ContactTypeattributes, but with one important difference You, as a developer, can’t reallyanticipate how users are going to want to group the Entries in their AddressBooksthe way you could anticipate the contact types It’s obvious then, that the
AddressBook class needs some way of modifying the list of Groups besides editingthe class’ source code in AddressBook.py In other words, the Group list must beconfigurable via its own management screens
There is another design decision to be made here: whether the group managementscreens should be end-user screens or Zope management screens The decisiondepends on who you see adding, deleting, and renaming groups — a site administra-tor or an end user We are going to proceed under the assumption that the end usershould be able to manage the groups
There are three modifications that need to be made to the AddressBook:
✦ AddressBook needs a manageGroupFormDTML method
✦ AddressBook needs a addGroupmethod
✦ AddressBook needs a delGroupsmethod
First, add a manageGroupForm.dtmlfile to the /Addressit/dtml/folder with thefollowing code:
<dtml-var standard_html_header>
<dtml-var standard_addressbook_header>
<h2><dtml-var title></h2>
<h3>Manage Groups</h3>
<form action=”.” method=”post”>
<table border=0 cellpadding=3 cellspacing=0>
Trang 16This checks to see whether the current sequence-item number is even, and if so, adds
a gray background color to the table row The upshot of this little bit of code is that
every other row is gray, which is a technique known as greenstriping after the green
and white striped printout paper that was used in dot-matrix printers Whether in aprintout or on-screen, greenstriping increases the legibility of information in tabularformat, which is a common usability improvement in Web applications
Next, you will need to make some changes to the AddressBook.pyfile First, edit theend of the file (where the Web methods are defined) to add the manageGroupForm
Web method:
# Web Presentation Methods index_html = DTMLFile(“dtml/indexAddressBook”, globals()) addEntryForm = DTMLFile(“dtml/addEntryForm”, globals()) standard_addressbook_header = DTMLFile(“dtml/StandardAddressBookHeader”, Æ globals())
manageGroupForm = DTMLFile(“dtml/manageGroupForm”, globals())
Next, edit the “Methods to manipulate groups” section to incorporate the addGroup
and delGroupsmethods outlined in Listing 8-1
Listing 8-1: addGroup and delGroups
# Methods to manipulate groupsdef addGroup(self, group, REQUEST = None):
“Method to add Groups”
if group not in self.GroupList:
self.GroupList.append(group)
Continued
Trang 17Listing 8-1 (continued)
self. changed (1)
if REQUEST is not None:
return self.manageGroupForm(self, REQUEST)
def delGroups(self, groups = [], REQUEST = None):
“method to delete groups”
if type(groups) != ListType:
groups = [groups]
for group in groups:
if group == ‘Unfiled’: continue
# You are not allowed to delete Unfiledtry:
index = self.GroupList.index(group)del self.GroupList[index]
except ValueError:
passself. changed (1)
if REQUEST is not None:
return self.manageGroupForm(self, REQUEST)
None, and if it isn’t, (meaning that the addGroupsmethod was invoked through theWeb), the method returns the manageGroupFormsmethod
The delGroupsmethod is a little more complex It takes a list of group ids in theparameter “groups.” The manageGroupFormpage identifies each listed Group submit-ted via checkbox whose name is set to “groups:list.” This is a technique wherebyZope will coerce the submitted values to a list, even if there is only one checkboxchecked Meanwhile, in a suspenders-and-belt fashion, the delGroupsmethod alsochecks to see whether the group’s parameter submitted to it is a list, and if not,changes it into one This is done to ensure that the correct type is iterated over,even if the method is called from somewhere else other than the manageGroupForm
page
Trang 18After delGroupshas ensured that it is operating on a list, it iterates over that list totry and delete each group in turn, but first it checks to see whether the group it istrying to delete is the “Unfiled” group As this is the group that Entries are set to bydefault upon their creation, deleting this group would probably be a bad thing, sothe method will skip over an attempt to delete this Group.
Next, the delGroupsmethod tries to find the index of the current Group inGroupList, and tries to delete the list item of that index If the index lookup fails(and raises a ValueError), this would indicate that the list does not actually have agroup of that name stored This situation would pretty much only occur if someoneelse had already deleted the group in question sometime between when the
manageGroupFormwas rendered and when that particular group was iterated over in
delGroups So, delGroupswill skip over the group if this exception is raised
And that’s it! If you refresh the Addressit product after saving the open files, youshould be able to go to /Test/manageGroupFormin order to see the group manage-ment interface, as shown in Figure 8-6:
Figure 8-6: The manageGroupForm interface
Now that the user has an easy way to add and delete Groups, there is some clean
up that needs to be done The AddressBook should no longer be initialized with thetest1 and test2 values in the GroupList attribute, only with Unfiled So we need tomake the following change to init :
Trang 19def init (self, id, title):
self.id = idself.title = titleself.ContactTypeList = [‘Email’, ‘Home’, ‘Mobile’,
‘Work’, ‘Pager’, ‘ICQ/IM’,
‘URL’, ‘Extension’]
self.Entries = {}
self.LastEntryID = 0self.GroupList = [‘Unfiled’]
You should also change StandardAddressBookHeader.dtmlto link to the
manageGroupFormmethod and generally improve the application-level navigation:
<table width=”100%” cellspacing=”0” cellpadding=”2” border=”0”>
<dtml-if “meta_type==’AddressBook Entry’”> | Æ
<a href=”./editEntryForm”>Edit Entry</a></dtml-if>
Otherwise, if the meta-type is something else, the link is created to point towards
/, or in other words, the default view object in the parent directory This means
that whether the standard_addressbook_headeris rendered in the context of theAddressBook (or one of its methods) or in the context of an Entry (or one of theEntry’s methods) the link will always point correctly towards the AddressBook’s
index_htmlmethod, and not toward the Entry’s index_htmlmethod
Similarly, the code for creating the second link will always point toward theAddressBook’s manageGroupFormmethod
The code creating the third link only renders if the current context’s meta-type is
“AddressBook Entry,” and creates a link to the current Entry’s editEntrymethod.Figure 8-7 and Figure 8-8 show the improved application navigation:
Trang 20Figure 8-7: The Improved AddressBook Navigation 1
Figure 8-8: The Improved AddressBook Navigation 2
Trang 21The simplicity of the code to create this navigation bar is one of the ways in whichZope demonstrates the power of its approach toward object publishing Becauseobject containment can be reflected directly in the URL, creating a link that leads tothe containing object is as simple as pointing the link towards the parent directory.The code for the product at this point can be found in the /chapter_08/ Addressit_6directory on the accompanying CD-ROM.
Retrieving Entries by Group
Now that you can categorize Entries by Group, we’ll show you how to enhance theAddressBook class and the user interface to retrieve Entries according to whichgroup the entries are in
First, the listEntriesmethod must be replaced with a listEntriesByGroup
method The new method will take a groupparameter whose default will be “All”:.Remove the listEntriesmethod from AddressBook.py, and replace it with thefollowing code:
def listEntriesByGroup(self, group = “All”):
ret = []
if group == “All”:
for entry in self.Entries.values():
ret.append(entry)else:
for entry in self.Entries.values():
if entry.Group == group:
ret.append(entry)return ret
Note that if the group parameter equals All, the listEntriesByGroupmethod hasidentical functionality to the original listEntriesmethod (returning all Entries).But if groupequals some other value, the method iterates over the list of Entryobjects, examines each Entry’s Groupattribute, and only appends it to a list object(labeled ret) if the Groupattribute’s value equals the value of the groupparameter.After all entries that match this criteria are appended to the retlist, the list isreturned by the method
Changing the name of the method by which Entries are retrieved necessitates somechanges to indexAddressBook.dtmlas well
First, add the following code to the beginning of the file, between the headers andthe code that set’s the default start value:
On the CD-ROM
Trang 22Now, pass the SelectedGroup parameter in the URL like this: /Test?SelectedGroup=Bad,and you should now see the filtered list of Entries that correspond to the appropriateGroup (Remember that the Group attribute is case-sensitive.)
However, if you have more than one batch of Entries displayed and want to navigateamong them, you’ll notice that clicking on any of the batch navigation links resetthe SelectedGroup parameter back to the default, and so causes the resulting page
to display all of the Entries again Clearly this is not what you want In order to serve the SelectedGroup parameter, you must cause the parameter to be passedalong in the URL of the batch navigation links, just like the start parameter Rewriteall of the links in indexAddressBook.dtml that look like this:
pre-<a href=”&dtml-absolute_url;?start=&dtml-batch-start-number”>
to look like this, instead:
<a href=”&dtml-absolute_url;?start=&dtml-batch-start-Ænumber;&SelectedGroup=&dtml-SelectedGroup;”>
The batch navigation links should now have the expected behavior when navigating
<input type=”submit” name=”index_html:method” value=”View”>
<select size=1 name=”SelectedGroup”>
<option value=”All”>All Groups
Trang 23Your indexAddressBook.dtmlfile should now look like the code in Listing 8-2.
</dtml-in>
</dtml-in>
<dtml-in “listEntriesByGroup(_[‘SelectedGroup’])” Æsize=20 start=start>
Trang 24<dtml-in next-batches mapping>
[<a href=”&dtml-absolute_url;?start=&dtml-batch-start-number;Æ
&SelectedGroup=&dtml-SelectedGroup;”><dtml-var Æbatch-start-number> - <dtml-var batch-end-number></a>]
</dtml-in>
</dtml-in>
<form action=”.” method=”post”>
<input type=”submit” name=”index_html:method” value=”View”>
<select size=1 name=”SelectedGroup”>
<option value=”All”>All Groups
<input type=”submit” name=”delEntries:method” value=”Delete”>
<input type=”submit” name=”addEntryForm:method”
value=”Add”><br>
<dtml-in “listEntriesByGroup(_[‘SelectedGroup’])” Æsize=20 start=start sort=LastName,FirstName,MiddleInitial>
<input type=”checkbox” name=”EntryIDs:list” value=”&dtml-id;”>
<a href=”./&dtml-id;”><dtml-var title></a><br>
Trang 25Figure 8-9: The Group selection box
Renaming Groups
So, now that the users of the AddressBook have the ability to define Groups and set
a Group attribute on Entries (which is useful for filtering the Entry list displayed),
it is much easier for those users to manage and navigate a large AddressBook(hundreds or even thousands of Entries) But another maintenance headache cannow rear its ugly head
What do users do if they want to rename one of the Groups? With the current sion, a user would have to create a new Group, go through the list of the old Group,and edit the individual Entries to use the new Group name instead of the old one.Once the old Group was empty, it could be safely deleted
ver-This is an incredible pain in the tuchis (Yiddish for butt) if the group in question has
more than a few Entries in it, so let’s add a renameGroupsmethod to the AddressBookclass to take the drudgery out of this maintenance chore Add the following code intothe section of the AddressBook class that is labeled “# Methods to add, edit, deleteand retrieve Entries” between the delGroupsmethod and the listEntriesByGroup
method:
Trang 26def renameGroup(self,
OldGroupNames,NewGroupName,REQUEST = None):
“Method to rename one or more groups”
if type(OldGroupNames) != ListType:
OldGroupNames = [OldGroupNames]
self.addGroup(NewGroupName)for OldGroupName in OldGroupNames:
if OldGroupName != ‘Unfiled’:
self.delGroups(OldGroupName)for entry in
self.listEntriesByGroup(OldGroupName):
entry.Group = NewGroupName
if REQUEST is not None:
return self.manageGroupForm(self, REQUEST)The renameGroupsmethod takes an OldGroupNamesparameter (which should be alist) and a NewGroupNameparameter, as well as the now familiar REQUEST = None.First, the method makes sure that the OldGroupNameis in fact a list, and if it isn’t alist, the method changes it into one It then adds the NewGroupNameto GroupList.Then the renameGroupmethod iterates over the OldGroupNameslist, and for each
OldGroupNamedoes the following:
✦ Deletes the GroupNameif it’s not “Unfiled”
✦ Uses listEntriesByGroupto retrieve all of the Entries that match
OldGroupName
✦ Iterates over that list, setting each Entry’s Groupattribute to NewGroupName
After each inner loop in which the members of the OldGroupNameare moved to the
NewGroupName, the method increments the outer loop to move on to the next
OldGroupName After all of the OldGroupName’s are done, the method checks to seewhether it was called through the Web, and if so, returns the manageGroupFormview
This renameGroupsmethod is a miniature study of code reuse in a way The method isusing two methods that we’ve previously defined: delGroups(used to delete the
OldGroupName) and listEntriesByGroup(used to retrieve a subset of Entry objectsthat match OldGroupNamefor reassignment to NewGroupName) The listEntriesByGroup
method was never intended to be called through the Web, but it is easy to see now
Trang 27why the delGroupsmethod and all other methods that could be called that way need
to check and see whether in fact they were If the delGroupsmethod had not been set
up to check whether it had been called through the Web, it would have returned themanageGroupForm view early (to the renameGroupsmethod), which would havecaused an error
There are two more changes you need to make to enable this feature The first is toadd an appropriate submit button that will lead to a renameGroupForm To do so,rewrite the manageGroupForm.dtmlfile as follows:
<dtml-var standard_html_header>
<dtml-var standard_addressbook_header>
<h3>Manage Groups</h3>
<form action=”.” method=”post”>
<table border=0 cellpadding=3 cellspacing=0>
<dtml-in listGroups>
<tr <dtml-if sequence-even>bgcolor=”#CCCCCC”</dtml-if>>
<td><input type=checkbox name=”groups:list” value=”&dtml-sequence-item;”></td>
<td><a href=”&dtml-sequence-item;”><dtml-var sequence-item></td>
</tr>
</dtml-in>
<tr><td colspan=2>
<input type=submit name=”delGroups:method” value=”Delete selected Groups”>
<input type=submit name=”renameGroupForm:method” value=”Rename selected Æ Group”><br>
<input type=text name=”group”><input type=submit name=”addGroup:method” Æ value=”Add a Group”>
Old Group Name: <dtml-var groups><br>
New Group Name: <input type=”text” name=”NewGroupName”>
<input type=”submit” value=”Rename Group”>
</form>
<dtml-var standard_html_footer>
Trang 28The code here is not quite obvious In order to take the selected Groups (that hadtheir checkboxes checked) from the previous form and pass them along to the
renameGroupsmethod, the “groups” form value is iterated over here to create ahidden form field for each checked group The hidden fields all have the name
OldGroupNames:list, which again causes Zope to force the submitted values to alist, even when there is only one This form also has a NewGroupNametext field and asubmit button
Now you just need to add a declaration in the “# Web presentation methods”
section of the AddressBook class to enable the new functionality Add the followingcode to the end of the AddressBook.pyfile:
renameGroupForm = DTMLFile(“dtml/renameGroupForm”, Æglobals())
One of the interesting things about the way the renameGroupfeature works, is that
by checking more than one group name on the manageGroupForm, you not onlyrename several groups at once, but also their Entries are consolidated into the newGroup as well So, for example, if you have a “Competitors” group and a “Rivals”
group, you can check them both, and click the Rename selected groups button
After you are presented with the renameGroupsForm, you can fill in the new groupname as “Enemies,” and click the submit button Voila! You’ve combined both of theold Groups into a single new Group
The code for the product at this point can be found in the /chapter_08/
Addressit_7directory on the accompanying CD-ROM
Sorting Entries by Column
You’ve made a lot of user interface and usability improvements in the AddressBookthroughout this chapter, but the main view of the AddressBook could still use somework
First, change the rendering code for the entry list so it displays the list in a moreinformative and usable way, as in Listing 8-3
Trang 29<form action=”.” method=”post”>
<table border=0 cellpadding=2 cellspacing=0 width=”100%”>
<select size=1 name=”SelectedGroup”>
<option value=”All”>All Groups
<dtml-in listGroups>
<option value=”&dtml-sequence-item;”<dtml-if ÆSelectedGroup><dtml-if “_[‘sequence-Æ
item’]==SelectedGroup”>selected</dtml-if></dtml-if>><dtml-var Æsequence-item>
</dtml-in>
Trang 31This improved version of the AddressBook’s index_htmlmethod makes a number ofchanges as you can see in Figure 8-10.
Figure 8-10: Improved index_html for the AddressBook
The improvements in this version are easily summed up Starting from the top ofthe page and working down and left-to-right the improvements are:
✦ The form buttons, batch navigation, and Entry list are now in a single table
✦ The group selection drop-down menu was moved to the right, and an ellipsis( ) button was added to point to manageGroupForm
✦ The batch navigation was moved below the form buttons and centered
✦ The checkboxes and name for each entry are now in separate columns of thetable
✦ Three more columns were added to the table, one each for the Entries’ Title,Company, and another for an edit link pointing to the Entry’s editEntry view
✦ The Name, Title, and Company columns of the table have appropriate headers.You can see that this has improved the usability of the AddressBook, even though
we haven’t added any new functionality yet
Trang 32Now that we’re displaying three separate columns for each Entry (not counting thecolumns that contain the checkboxes and edit links), it would be nice to let userssort the Entries according to whichever column they find most convenient.
This is usually done by making the column titles into links First though, you need
to create and set a sort_byvariable in the page so that the page can have a default
Add the following code just after the code that sets the start variable at the ning of the indexAddressBook.dtmlfile:
begin-<dtml-unless sort_by>
<dtml-call “REQUEST.set(‘sort_by’,
‘LastName,FirstName,MiddleInitial’)”>
</dtml-unless>
Now change the batch navigation <dtml-in>tags to include a sort_expr=”sort_by”
attribute and replace the sort=FirstName,LastName,MiddleInitialfrom the mainEntries loop with a sort_expr=”sort_by”as well
We’re using a form of indirection here The sort_exprattribute in the <dtml-in>tagindirectly gets the sorting string from sort_by The expression in quotes for thisattribute must be a valid Python expression, but the expression is not evaluated foreach sequence-item to calculate the value that the items will be sorted by as youmight think Instead, the expression is evaluated once, and the result must be avalid sorting string, just as if you had passed it to the sort attribute So, the defaultvalue of sort_byis FirstName,LastName,MiddleInitial(no spaces) and when the
<dtml-in>tag evaluates sort_expr=”sort_by”, sort_byis evaluated to its containedvalue, and the <dtml-in>tag behaves just as it had been told to
sort=FirstName,LastName,MiddleInitial
If all this rigmarole to get the same sorting result seems unnecessary, rememberthat the code you added to the beginning of the file which sets the default sortingorder can be easily overridden by setting the sort_byattribute before the codeexecutes
For example, you can pass in a sort_byparameter in the URL to change how theEntries are sorted Rewrite the URL to /Test?sort_by=Companyand you’ll see thatthe Entries are now sorted differently
So, now you need to expose this sorting functionality to the users, as you don’twant them to have to rewrite URLs to sort the Entries
Change the section of the file that creates the column headings to read as follows:
<tr>
<td> </td>
<th><a href=”&dtml- _ absolute_url;?sort_by=LastName,FirstName,MiddleInitial&SelectedGroupÆ
=&dtml- SelectedGroup;”>Name</a></th>