Design a user-friendly form to update multiple ReferenceFields
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. 
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:
- Navigate to ShrubberyClipping#1, go to the edit tab, set Row from B to I, save.
- Navigate to ShrubberyClipping#2, go to the edit tab, set Row from B to I, save.
- Navigate to SwallowCell#1, go to the edit tab, set Row from I to B, set Col from 9 to 1, save.
- Navigate to SwallowCell#2, go to the edit tab, set Row from I to B, set Col from 8 to 2, save.
- 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.

Author: