Attention

This document was written for an unsupported version of Plone, Plone 2.1.x, and was last updated 898 days ago.

For more information, see the version support policy.

To learn how to upgrade to the current version of Plone, read the upgrade manual.

Design a user-friendly form to update multiple ReferenceFields

by Rob Lineberger last modified Dec 06, 2009 09:27 PM
The end result of this how-to is a design pattern for making a customized widget to wrangle an array of AT Reference Fields. The true purpose of the how-to is to document one process for semi-technical Plone developers to move from problem to solution.

This how-to has a dual purpose: to solve a usability and Archetypes design issue, and also to show how a semi-literate developer might step through AT development problem solving.

The problem:

We're using Plone to store inventory information for a scientific lab.  There is a specific type of freezer storage box (FreezerColumnBox) that contains a 9 x 9 grid for holding test tubes (represented by an AT called LabMaterial). In most cases, the specific cell where a LabMaterial is located is not important to track: scientists move these tubes around frequently, and simply knowing which box it is in is sufficient. But in some cases (such as liquid nitrogen storage, or in the case of archived samples) it is critical to know precisely which of the 81 cells holds a specific LabMaterial. boxgrid


First attempt at a solution: Row and Col Properties with CSS Display

One answer is rather simple, and will be obvious to anyone who has played Battleship: give each LabMaterial a row property and a column property.  This simple coordinate system is intuitive and time-honored.  To do this, we added the following VOCABULARIES to the config.py file of our AT Product:

ROW = DisplayList((
('none', '--'),
('rowa', 'Row A'),
('rowb', 'Row B'),
('rowc', 'Row C'),
('rowd', 'Row D'),
('rowe', 'Row E'),
('rowf', 'Row F'),
('rowg', 'Row G'),
('rowh', 'Row H'),
('rowi', 'Row I'),
))

COL = DisplayList((
('none', '--'),
('col1', 'Column 1'),
('col2', 'Column 2'),
('col3', 'Column 3'),
('col4', 'Column 4'),
('col5', 'Column 5'),
('col6', 'Column 6'),
('col7', 'Column 7'),
('col8', 'Column 8'),
('col9', 'Column 9'),
))

And then provide fields and widgets in the schema to set them:

    StringField('row',
vocabulary = ROW,
required = 0,
widget=SelectionWidget(
label='Row',
format='select'
),
),

StringField('col',
vocabulary = COL,
required = 0,
widget=SelectionWidget(
label='Column',
description ='',
format='select'
),
),

So far so good.  We can now specify a row and column for each LabMaterial.  But we need some way to display this to the user.  Fortunately, a combination of page templates and CSS provides a relatively straightforward approach.  In CSS, you can set the class property of an element, which tells the browser how to display that element.  So we could build a CSS div called "dynacell" that looks like a gridded box (by putting the image above in the background), and then displaying 40 x 40 pixel hyperlinks on top of it to show where the stuff actually is.

For example, we could build a class called rowa (which is the value stored in our widget for things on Row A) and set its top property to 0px. We could build a class called rowb and set its top property to 40px, then rowc to 80px, and so on.  Each row would be 40 pixels tall, and the LabMaterials would show up on the right row based on the value the LabMaterial has in its Row property. In theory, it would look like this:

   <a href="someLabMaterialStoredInRowA"  class ="rowa">Some LabMaterial Stored in Row A</a>

In a page template it would look more like the code below, where the href attribute points to the id of the LabMaterial, the class attribute gets set to the row, and for a bonus, the title attribute gets set to the description so that when users hover over the link they can see what LabMaterial they're looking at:

   <a tal:attributes="href string:${box/id} ; class string:${box/getRow} ; title string:${box/getDescription}">
       <b tal:content="box/title_or_id" />
</a>

The better news is that you can assign more than one class to an html element.  This means you can dynamically show both the row and col of any LabMaterial by simply structuring your links like this:

<a href="LabMaterialStoredInRowAandCol1"  class ="rowa col1 ">Some LabMaterial Stored in Row A and Column One</a>

Or in the page template, the list of links would look like this:

  <b>Box Contents:</b>
  <div class="dynacell">
    <img src="boxgrid.gif" />
    <tal:block
       tal:repeat="box python:here.contentValues(['LabMaterial'])">
            <a tal:attributes="href string:${box/id} ; class string:${box/getCol} ${box/getRow} ; title string:${box/getDescription}">
              <b tal:content="box/title_or_id" />
</a>
    </tal:block>
   </div>

The complete CSS for this solution looks like this:

div.dynacell {
padding: 0 px;
border: 0px double red;
margin: 0px;
position: relative;
background-color: white;
display: block;
}

.dynacell a {
position: absolute;
padding: 4px;
margin: 0px;
border: 2px dotted;
background-color: #BADEF7;
border-color: #666666;
color: black;
font-weight: bold;
text-align: center;
font-size: -1;
overflow: hidden;
width: 40px;
height: 40px;
}

.dynacell a:hover {
background-color: #6699CC;
border-color: black;
color: white;
}

.dynacell a.rowa{ top: 28px ; width: 40px; height: 40px }
.dynacell a.rowb{ top: 78px ; width: 40px; height: 40px }
.dynacell a.rowc{ top: 128px ; width: 40px; height: 40px }
.dynacell a.rowd{ top: 178px ; width: 40px; height: 40px }
.dynacell a.rowe{ top: 228px ; width: 40px; height: 40px }
.dynacell a.rowf{ top: 278px ; width: 40px; height: 40px }
.dynacell a.rowg{ top: 328px ; width: 40px; height: 40px }
.dynacell a.rowh{ top: 378px ; width: 40px; height: 40px }
.dynacell a.rowi{ top: 428px ; width: 40px; height: 40px }
.dynacell a.rowj{ top: 478px ; width: 40px; height: 40px }
.dynacell a.rowk{ top: 528px ; width: 40px; height: 40px }

.dynacell a.col1{ left: 28px ; }
.dynacell a.col2{ left: 78px ; }
.dynacell a.col3{ left: 128px ; }
.dynacell a.col4{ left: 178px ; }
.dynacell a.col5{ left: 228px ; }
.dynacell a.col6{ left: 278px ; }
.dynacell a.col7{ left: 328px ; }
.dynacell a.col8{ left: 378px ; }
.dynacell a.col9{ left: 428px ; }

Armed with a row and column in our AT schema, and structuring the page template to assign the row and column to the class attribute of each link, and creating a css sheet to properly show the links, we now have a functional system to dynamically display exactly where the LabMaterials are in any given FreezerColumnBox.

Great! But... it doesn't work very well.


Let's say for the sake of argument that a scientist moves a group of 3 LabMaterials (say, African Swallow cells) from the bottom corner of the box to Row B.  There were a couple of shrubbery clippings at the end of Row B, which she moved to the bottom corner.  In our current design using standard Plone edit forms, she would have to record those movements by performing the following steps:

  1. Navigate to ShrubberyClipping#1, go to the edit tab, set Row from B to I, save.
  2. Navigate to ShrubberyClipping#2, go to the edit tab, set Row from B to I, save.
  3. Navigate to SwallowCell#1, go to the edit tab, set Row from I to B, set Col from 9 to 1, save.
  4. Navigate to SwallowCell#2, go to the edit tab, set Row from I to B, set Col from 8 to 2, save.
  5. Navigate to SwallowCell#3, go to the edit tab, set Row from I to B, set Col from 7 to 3, save.

Let's get really crazy and say that we decised to make a whole box dedicated to Shrubbery Clippings, and that Rows G-I were completely full.  To move the ShrubberyClippings in our box to that one, we'd have to cut them, navigate to the ShrubberyClippingsBox, paste them, then manually change the row/col properties of our new additions because they conflict with shrubberies already in the box.


In other words, our simple Row and Col fields on each LabMaterial aren't as useful as we initially thought.  As soon as a lab material moves into another box, its leftover row/col becomes a confusing liability.

Turning the Inside Out

Perhaps it would be better for a FreezerColumnBox to know where its contents are instead of the LabMaterial to know what row/col it's in.  I don't know; I'm asking. How would that look? One way would be to use schematas (which show up as separate subtabs on the edit form) and have nine ReferenceBrowserWidgets for each row schemata: 9 schematas x 9 ReferenceBrowserWidgets = 81 ReferenceFields.  Hmm... Not looking so user friendly.  It isn't a vast improvement over the previous design.

A New Widget?

Perhaps a new widget is called for.  Say, a ReferenceGridWidget.



Contribute

Something wrong or out of date? Anybody can edit or create a new article in the knowledge base. Simply create an account on this site, log in, and click the Edit button to contribute.