Opened 11 years ago

Closed 11 years ago

Last modified 11 years ago

#7329 closed defect (duplicate)

TabContainer: cannot set contents of hidden tab to be layout widget

Reported by: spidey2099 Owned by: bill
Priority: high Milestone: 1.2
Component: Dijit Version: 1.1.1
Keywords: Cc:
Blocked By: Blocking:

Description

A second tab container cannot be attached to tab 2 of a tab container. It can, however, be attached to tab 1. In fact tab containers can be nested one inside the other if they are attached to tab 1. Tab 2, on the other hand, does not appear to allow a tab container to be attached to it at all.

Attachments (12)

dojo-1.1-Test-4-TabContainerInnerExpandoPane.html (5.4 KB) - added by spidey2099 11 years ago.
Nest tab containers inside one another. Attach an expando to the innermost tab 1.
dojo-1.1-Test-4-TabContainerInnerExpandoPane.2.html (5.2 KB) - added by spidey2099 11 years ago.
The same test using the latest attach methodology. (<pane>.containerNode.appendChild (<container.domNode)
dojo-1.1-Test-4-TabContainerInnerExpandoPane.3.html (3.0 KB) - added by spidey2099 11 years ago.
7329.html (2.5 KB) - added by bill 11 years ago.
cleaned up test code a bit; seems to work for me (FF3/mac)
7329_fixed.html (2.5 KB) - added by bill 11 years ago.
moved startup() call to end to run after ContentPane? child is set.
dojo-1.1-Test-7-TabContainerInnerAccordionPane.html (4.2 KB) - added by spidey2099 11 years ago.
Tabs and Accordion panes. Test indicates inconsistence attach methodology leading to anomalous behavior.
dojo-1.1-Test-7-TabContainerInnerAccordionPane.2.html (5.1 KB) - added by spidey2099 11 years ago.
dojo-1.1-Test-7-TabContainerInnerAccordionPane.3.html (4.9 KB) - added by spidey2099 11 years ago.
dojo-1.1-Test-7-TabContainerInnerAccordionPane.4.html (5.6 KB) - added by spidey2099 11 years ago.
accordionTabs.html (4.8 KB) - added by dante 11 years ago.
cleaned up / better practices / workaround
accordionTabs2.html (3.9 KB) - added by dante 11 years ago.
for good measure, I've updaed your most recent patch to have the same best practices as the previous one
accordionTabs3.html (3.2 KB) - added by bill 11 years ago.
simplified test using setContent() but still fails on IE due to #5672

Download all attachments as: .zip

Change History (45)

Changed 11 years ago by spidey2099

Nest tab containers inside one another. Attach an expando to the innermost tab 1.

comment:1 Changed 11 years ago by bill

Component: DijitDojox
Owner: set to dante
Summary: Cannot attach a tabcontainer to tab 2.ExpandoPane: Cannot attach a tabcontainer to tab 2.

Changed 11 years ago by spidey2099

The same test using the latest attach methodology. (<pane>.containerNode.appendChild (<container.domNode)

comment:2 in reply to:  1 Changed 11 years ago by spidey2099

Replying to bill: This is not an expando problem. It is associated with the tab container. I am attaching the same test, excluding the expando code completely. The problem remains the same.

comment:3 Changed 11 years ago by bill

Component: DojoxDijit
Summary: ExpandoPane: Cannot attach a tabcontainer to tab 2.TabContainer: Cannot attach a tabcontainer to tab 2.

I don't really understand your test or your comment. When I click tab 2 I do see sub tabs, although nothing is in them.

I cleaned up your test case a bit; i'll attach the new one.

Changed 11 years ago by bill

Attachment: 7329.html added

cleaned up test code a bit; seems to work for me (FF3/mac)

comment:4 in reply to:  3 Changed 11 years ago by spidey2099

Replying to bill:

I don't really understand your test or your comment. When I click tab 2 I do see sub tabs, although nothing is in them.

I cleaned up your test case a bit; i'll attach the new one.

I will try the cleaned up code. However, what build/release are you using? I am using the nightly build from 7/15/2008, running on SuSE 10.0 Linux/FF2. The sub-tab never appears in Tab 2.

comment:5 Changed 11 years ago by bill

I'm running with the latest code on FF3/mac.

comment:6 in reply to:  5 Changed 11 years ago by spidey2099

Replying to bill:

I'm running with the latest code on FF3/mac.

The code provided in your attachment was tested against the 7/15/2008 nightly and did not work. However, when tested against the 7/31/2008 nightly, it did work - sort of. I didn't see the borders of the content panes in the sub-tab. Perhaps that works in FF3, but we shouldn't have to go to FF3 to have this functionality work. More testing in progress.

Also, I have a recommendation: We really need to have policies around attach methodologies and other programmatic constructs. These guidelines and policies need to be put into the dojo API site together with examples on how to programmatically construct and wire-up dojo classes. If folks like myself could post to the API site, we could help in this effort. What do you think?

comment:7 Changed 11 years ago by bill

Owner: changed from dante to bill

OK, yeah, it's not a FF2/FF3 or mac vs. linux thing. I went over this more closely and the problem is related to your call:

parent.containerNode.appendChild (tabContainer.domNode);

which happens after startup() has been called on the main tabContainer.

parent in this case is a ContentPane, and it doesn't realize that it's contents have changed (the key is that it doesn't realize it contains a single layout widget and when it should forward startup()/resize() calls to that layout widget).

I moved the startup() call to the end and now it works for me.

Changed 11 years ago by bill

Attachment: 7329_fixed.html added

moved startup() call to end to run after ContentPane? child is set.

comment:8 in reply to:  7 ; Changed 11 years ago by spidey2099

Replying to bill:

OK, yeah, it's not a FF2/FF3 or mac vs. linux thing. I went over this more closely and the problem is related to your call:

parent.containerNode.appendChild (tabContainer.domNode);

which happens after startup() has been called on the main tabContainer.

parent in this case is a ContentPane, and it doesn't realize that it's contents have changed (the key is that it doesn't realize it contains a single layout widget and when it should forward startup()/resize() calls to that layout widget).

I moved the startup() call to the end and now it works for me.

Yes - this works - but it surfaces a bigger issue: what works for tab 1, doesn't work for tab 2. Also, if attaching a border container, the tab container must startup() before the border container is attached. If not, the expandos within the border container collapse all the way, regardless of what tab they are attached to.

This highlights a contention of mine: there must be a consistent attach methodology for all widgets. This results in a better programming model. Otherwise, each page configuration will have idiosyncratic attach methods unique for that page. This is not cool as it puts the burden on dojo's users to test every combination and work it into their page design.

Changed 11 years ago by spidey2099

Tabs and Accordion panes. Test indicates inconsistence attach methodology leading to anomalous behavior.

comment:9 Changed 11 years ago by spidey2099

Either I am missing something or the TabContainer? has something fundamentally broken within it.

comment:10 Changed 11 years ago by spidey2099

Please note: this has high priority. Containers must be able to attach to other containers iteratively and not sequentially. Pages can and must change after they have been instantiated. The notion that a page is built and then remains static for it's life span is no longer true. RIAs with their component model should allow for containers to attach to their parents (through content panes or expando panes, etc) in an iterative manner at any time during the pages life cycle. Example: construct a border container. Later, attach a tab container. Attach a tree to a pane in the border container. These hook-ups do not occur all at once at startup time. Instead, they may and will occur based of Events, after a page is built. This is a must have for RIAs. So the attach methodology must be clear and consistent.

Without this the application I am building will no longer work!!! Perhaps I should come by an show you what I mean, if that's possible.

comment:11 in reply to:  8 ; Changed 11 years ago by bill

Replying to spidey2099:

Yes - this works - but it surfaces a bigger issue: what works for tab 1, doesn't work for tab 2.

No, it really wasn't working for any tab, on any browser ; if it seemed to show up correctly on the screen for you that's just lucky.

The supported API for ContentPane is to either specify the content initially, or to change it using setContent(). Anything else you do is at your own risk.

Also, if attaching a border container, the tab container must startup() before the border container is attached. If not, the expandos within the border container collapse all the way, regardless of what tab they are attached to.

I'm sure that's not true as it mean that declarative markup doesn't work. Probably an error in your code, but you'd need to attach a test case to be sure.

comment:12 in reply to:  11 Changed 11 years ago by spidey2099

Replying to bill:

Replying to spidey2099:

Yes - this works - but it surfaces a bigger issue: what works for tab 1, doesn't work for tab 2.

No, it really wasn't working for any tab, on any browser ; if it seemed to show up correctly on the screen for you that's just lucky.

The supported API for ContentPane is to either specify the content initially, or to change it using setContent(). Anything else you do is at your own risk.

Also, if attaching a border container, the tab container must startup() before the border container is attached. If not, the expandos within the border container collapse all the way, regardless of what tab they are attached to.

I'm sure that's not true as it mean that declarative markup doesn't work. Probably an error in your code, but you'd need to attach a test case to be sure.

The tests I have provided already demonstrate this. There is also a bug I have reported on this problem - 7299. Also - please look at the last attached test which demonstrates this using the accordion pane. Try uncommenting some of the suggested statements and see what happens. Making the assumption that what works in declaritive markup, works programatically is just that - an assumption. I would ask you to prove that I am wrong as I have tested this premise and found it to be fatally flawed. And it has cost me hundreds of hours of painful experimentation without lack of any documentation on how to wire up dojo components. Don't make your users wrong without checking what they are saying. Test your assumptions and find out what really does or does not work.

comment:13 Changed 11 years ago by bill

I looked at the accordion test you attached above. It's got the problem I listed in my previous comment, with this code:

parent.containerNode.appendChild (container.domNode);

Making the assumption that what works in declaritive markup, works programatically is just that - an assumption. I would ask you to prove that I am wrong

Sure. All the parser does is to look at the existing DOM and make calls to new TabContainer(), new AccordionContainer(), etc. So, the parser *is* creating everything programatically.

comment:14 in reply to:  13 Changed 11 years ago by spidey2099

Replying to bill:

I looked at the accordion test you attached above. It's got the problem I listed in my previous comment, with this code:

parent.containerNode.appendChild (container.domNode);

Making the assumption that what works in declaritive markup, works programatically is just that - an assumption. I would ask you to prove that I am wrong

Sure. All the parser does is to look at the existing DOM and make calls to new TabContainer(), new AccordionContainer(), etc. So, the parser *is* creating everything programatically.

That's good to know. However, here's the rub. Embedded code is laid out sequentially and is parsed one time from the root of the tree down to the leaves. That is one use case. Imagine this: A page is added with a Border container and a tab container. Now some event occurs and I want to attach a BorderContainer? with expandos to a one of the tabs. What happens. This is an event based or asynchronous model as opposed to a sequential of synchronous use case. My question is: what happens in the asynchronous use case.

Also - here's what I did to retest the accordion container. It is attached as test 7 again.

comment:15 Changed 11 years ago by bill

Right, there are multiple scenarios for programmatic creation, including adding content to a TabContainer etc. where TabContainer.startup() has already been called. (That's why there's no one "canonical" method for how to do programmatic creation.) It's possible some of them are broken, including maybe some AccordionContainer specific bugs. Is that the case you are trying to do, adding an AccordionContainer to an already started TabContainer?

As for your reattached test case, I'm not sure why you are setting up a TabContainer --> ContentPane (cp2) --> ContentPane (innerContainer0) --> AccordionContainer? hierarchy? You can just do TabContainer --> AccordionContainer. Unless it's to try to get margin between the TabContainer border and the AccordionContainer border?

In theory it should work though.

Also BTW dojo.byId (parent.id) == parent.

comment:16 Changed 11 years ago by bill

Resolution: invalid
Status: newclosed

I looked over your test case more closely. The problem is that you have a ContentPane called cp2 which contains both some free text (Content for CP2) and an AccordionContainer. So I don't see any issue with dijit. If you are still seeing a problem after that, then try to come up with a simpler test case and reopen this ticket.

comment:17 in reply to:  16 Changed 11 years ago by spidey2099

Resolution: invalid
Status: closedreopened

Replying to bill:

I looked over your test case more closely. The problem is that you have a ContentPane called cp2 which contains both some free text (Content for CP2) and an AccordionContainer. So I don't see any issue with dijit. If you are still seeing a problem after that, then try to come up with a simpler test case and reopen this ticket.

Bill - I guess what I was trying to state did not come through. I have attached a test which demonstrates my point more clearly. At least I hope so.

I have added a button to the test which adds an accordion container asynchronously - i.e through an event. In the function constructTabContainer() I have hard coded the attach target to "0.1" or "0.2", tabs 1 and 2 respectively. (Change the "0.1" target to "0.2" and then rerun the test). The test demonstrates that the accordion container is successfully attached to tab 1, but does not do so for tab 2. It is attached in a closed state and does not open.

Please tell me what I am missing.

comment:18 Changed 11 years ago by spidey2099

Bill,

Here's the problem - the target tab must be open for the attach to work. I have attached a test that demonstrates this. The Build button will generate events to add a accordion to tab 1 or tab 2. If the tabs are open before the addition occurs, all is well. But if the tab is closed (either tab 1 or tab 2), the accordion container is added in a closed state.

comment:19 Changed 11 years ago by dante

spidey2099: I think this is not a TabContainer? bug, but simply an AccordionContainer? limitation I am supposed to be fixing. fwiw, I am getting errors trying your most recent attachment, 'cid' is undefined. hacking in a value, I'm able to make the test "break" as you are mentioning. Its breaking because the tab simply isn'y visible, and the AccordionContainer? is unable to calculate the size it should be. If you select Tab 2, then create it, it renders properly. I have updated your test to reflect some better practices, and worked in a hacky workaround (which I know is a less-than-ideal situation, but works for now, until I fix accordion). An alternative would be to listen to the the pane's selectChild method, and call .layout() on inner layout widgets when they become visible.

Changed 11 years ago by dante

Attachment: accordionTabs.html added

cleaned up / better practices / workaround

Changed 11 years ago by dante

Attachment: accordionTabs2.html added

for good measure, I've updaed your most recent patch to have the same best practices as the previous one

comment:20 in reply to:  19 Changed 11 years ago by spidey2099

Replying to dante:

spidey2099: I think this is not a TabContainer? bug, but simply an AccordionContainer? limitation I am supposed to be fixing. fwiw, I am getting errors trying your most recent attachment, 'cid' is undefined. hacking in a value, I'm able to make the test "break" as you are mentioning. Its breaking because the tab simply isn'y visible, and the AccordionContainer? is unable to calculate the size it should be. If you select Tab 2, then create it, it renders properly. I have updated your test to reflect some better practices, and worked in a hacky workaround (which I know is a less-than-ideal situation, but works for now, until I fix accordion). An alternative would be to listen to the the pane's selectChild method, and call .layout() on inner layout widgets when they become visible.

Yes - sorry bout that. I had it working then removed the parent and cid from the arg list and posted it!!!! :-( I tried to get the latest version in there before that code got to you, but you guys were too quick. Anyway - thanks for looking into it, and thanks for the workaround. I look forward to the accordion container fix.

comment:21 Changed 11 years ago by bill

I think dante summed this up pretty well, except that I don't think this can be fixed in Accordion, because the Accordion has no way to calculate the height of each title bar if it's inside of a hidden div. I think this is a dup of #5672.

Here's a question: are you trying to set Tab 2 to be an accordion (and nothing but an accordion), or are you trying to make it an accordion mixed in with some existing markup. What *should* Tab 2 look like after pressing the button?

comment:22 in reply to:  21 Changed 11 years ago by spidey2099

Replying to bill:

I think dante summed this up pretty well, except that I don't think this can be fixed in Accordion, because the Accordion has no way to calculate the height of each title bar if it's inside of a hidden div. I think this is a dup of #5672.

Here's a question: are you trying to set Tab 2 to be an accordion (and nothing but an accordion), or are you trying to make it an accordion mixed in with some existing markup. What *should* Tab 2 look like after pressing the button?

Well - the accordion pane will have all kinds of content within it. But the problem is not just confined to Tab 2. The latest code (dojo-1.1-Test-7-TabContainerInnerAccordionPane?.4.html or dante's cleaned up version accordionTabs2.html) demos the fact that whenever an accordion is added to a closed Tab (1 or 2) it will not size correctly. So if I open tab 2 and add the accordion, it will attach and open as expected. Then, without opening tab 1, I add another accordion to tab 1. This time the accordion will attach, but remain closed. And of course the reverse is true as well.

In either case, there will be additional markup added to the accordion in either tab. They can and will have images, hrefs, or text, etc. This is because I am not making this for one project alone, but as a framework for more than one project.

comment:23 Changed 11 years ago by bill

Sure, the Accordion will have stuff in it, but it seems like you are saying the tab just contains an accordion? In your original test case the tab also had some text in it.

If the tab just contains an accordion then might be able to get it to work by replacing the unsupported call to:

parent.containerNode.appendChild (container.domNode);

in constructAccordionContainer() with a call to parent.setContent(container). (That only works in the latest code in SVN trunk.)

comment:24 in reply to:  23 Changed 11 years ago by spidey2099

Replying to bill:

Sure, the Accordion will have stuff in it, but it seems like you are saying the tab just contains an accordion? In your original test case the tab also had some text in it.

If the tab just contains an accordion then might be able to get it to work by replacing the unsupported call to:

parent.containerNode.appendChild (container.domNode);

in constructAccordionContainer() with a call to parent.setContent(container). (That only works in the latest code in SVN trunk.)

Okay - but tell me why the call parent.containerNode.appendChild (container.domNode) is unsupported. It was dante who provided me with that code in bug 7212 in an attachment called ep-prog.html. Is this code no longer valid for that use case as well? If so, does it mean that I should change all my attach methodology for content panes, expandos etc to use this latter methodology? Whew - it makes my head spin.

comment:25 Changed 11 years ago by bill

Because the ContentPane doesn't know that it's content has changed.

In this case ContentPane is analyzing the content to see if it contains a single layout widget and if so then when the ContentPane is resized (like when it is shown), it resizes it's single child.

You didn't answer my question above.

comment:26 in reply to:  25 Changed 11 years ago by spidey2099

Replying to bill:

Because the ContentPane doesn't know that it's content has changed.

In this case ContentPane is analyzing the content to see if it contains a single layout widget and if so then when the ContentPane is resized (like when it is shown), it resizes it's single child.

You didn't answer my question above.

Yes the tab container will, in this case, contain just an accordion. But my question is about a programming model. I hate to keep saying this, but if the attach methodology keeps changing, how are we to keep up, or even know what attach methodology we should be using? We need a normalized, standardized, canonical attach or wire-up methodology. Otherwise we are left guessing - and that impedes a projects forward movement.

comment:27 Changed 11 years ago by dante

spidey2099: fwiw, I supplied that #7212 code as a cleanup based on what you were doing already. (some things were plain "wrong", and others "workable" ... dojo/dijit is very flexible by nature of javascript) parent.containerNode is a supported dijit attachPoint, and is the content node for templated widgets. The whole point of it was to show what was possible above and beyond the supported dijit model (i am notorious for breaking dijit-supported functionality to suit my own needs, and was only trying to provide you with a workaround. go evil hackers!) ... This ticket seems out of hand as far as scope and reason, but contains SEVERAL very valid points. I encourage you to take this conversation up either more directly (i'm dante at dojotoolkit.org) with me, or in #dojo on irc.freenode.net ... the methodology does not keep 'changing' per se, it just has never been documented "how" as a best practice, and its hard to differentiate between "wrong" usage and bugs ... The declarative way "just works", and by nature the programmatic model is susceptible to MANY nuances. I would love for your enthusiasms and attention to detail in this matter to help actually _form_ a best practice, but in order for that to happen we need to eliminate some barriers. We simply need to document a lot of these things, and you seem to have a genuine interest, so again: I encourage you to get more directly involved. We try to keep trac to trace-able bugs, and put "discussion" about these things in the forums, dojo-contributors (are you signed up there? also, did you ever send a CLA in? I don't know your real name to associate it your nick) ... these test cases are very useful), or #dojo on irc.freenode.net (phiggins is me) ... but officially, 25 comments in a trac ticket with no resolve deems more direct conversation. This ticket should be closed, and the core of the issue discovered, documented, opened as a new issue, and potentially/hopefully fixed.

comment:28 Changed 11 years ago by bill

As Pete said the attach methodology isn't changing, just not well documented.

Layout widgets and other widgets with a simple list of children use addChild()/removeChild(), as they always have.

ContentPane has two usages:

  1. to hold free form content
  2. to hold a single layout widget

Mode (2) is special because when the ContentPane is resized, it resizes it's child to match; see the API documentation of ContentPane for details. Previously there was no officially supported way to set the content of a ContentPane to a single layout widget after the ContentPane had been started (startup()) or added to a TabContainer etc. that was started, so I added that functionality recently.

As for startup(), that has to be called on the top level layout widget in a hierarchy of layout widgets, after it's been attached to the DOM tree, and preferably as late as possible, but of course it's possible to (for example) start a TabContainer and then later do an addChild() to it, when the user presses a button. In that case startup() will be called automatically on the ContentPane so you don't need to worry about it.

comment:29 Changed 11 years ago by bill

Milestone: tbd1.3
severity: majornormal
Summary: TabContainer: Cannot attach a tabcontainer to tab 2.TabContainer: cannot set contents of hidden tab to be layout widget

FYI, I cleaned up/simplified the test, and it works perfectly on FF3 now, but as per problem in #5672 it won't initialize on IE. Attaching simplified test.

In the meantime I think you need to only create things in the currently displayed tab.

Changed 11 years ago by bill

Attachment: accordionTabs3.html added

simplified test using setContent() but still fails on IE due to #5672

comment:30 in reply to:  27 Changed 11 years ago by spidey2099

Replying to dante:

spidey2099: fwiw, I supplied that #7212 code as a cleanup based on what you were doing already. (some things were plain "wrong", and others "workable" ... dojo/dijit is very flexible by nature of javascript) parent.containerNode is a supported dijit attachPoint, and is the content node for templated widgets. The whole point of it was to show what was possible above and beyond the supported dijit model (i am notorious for breaking dijit-supported functionality to suit my own needs, and was only trying to provide you with a workaround. go evil hackers!) ... This ticket seems out of hand as far as scope and reason, but contains SEVERAL very valid points. I encourage you to take this conversation up either more directly (i'm dante at dojotoolkit.org) with me, or in #dojo on irc.freenode.net ... the methodology does not keep 'changing' per se, it just has never been documented "how" as a best practice, and its hard to differentiate between "wrong" usage and bugs ... The declarative way "just works", and by nature the programmatic model is susceptible to MANY nuances. I would love for your enthusiasms and attention to detail in this matter to help actually _form_ a best practice, but in order for that to happen we need to eliminate some barriers. We simply need to document a lot of these things, and you seem to have a genuine interest, so again: I encourage you to get more directly involved. We try to keep trac to trace-able bugs, and put "discussion" about these things in the forums, dojo-contributors (are you signed up there? also, did you ever send a CLA in? I don't know your real name to associate it your nick) ... these test cases are very useful), or #dojo on irc.freenode.net (phiggins is me) ... but officially, 25 comments in a trac ticket with no resolve deems more direct conversation. This ticket should be closed, and the core of the issue discovered, documented, opened as a new issue, and potentially/hopefully fixed.

Dante,

I agree that this ticket is too long as far as a conversation is concerned. I will definitely contact you directly by email. Also (yes I've promised this before) but I am going to send in the CLA shortly. Once that is done, I hope to provide dojo with some programmatic tests. Also - I don't mind helping to write documentation, especially if it goes into the API site. Folks do need to know the what, why and how of dojo's "best practices" - much like Bill's last entry in this conversation. From a users point of view, I can't tell you what a gift that would be to your community.

Best,

spidey2099 aka johnb

comment:31 in reply to:  28 Changed 11 years ago by spidey2099

Replying to bill:

As Pete said the attach methodology isn't changing, just not well documented.

Layout widgets and other widgets with a simple list of children use addChild()/removeChild(), as they always have.

ContentPane has two usages:

  1. to hold free form content
  2. to hold a single layout widget

Mode (2) is special because when the ContentPane is resized, it resizes it's child to match; see the API documentation of ContentPane for details. Previously there was no officially supported way to set the content of a ContentPane to a single layout widget after the ContentPane had been started (startup()) or added to a TabContainer etc. that was started, so I added that functionality recently.

As for startup(), that has to be called on the top level layout widget in a hierarchy of layout widgets, after it's been attached to the DOM tree, and preferably as late as possible, but of course it's possible to (for example) start a TabContainer and then later do an addChild() to it, when the user presses a button. In that case startup() will be called automatically on the ContentPane so you don't need to worry about it.

This info is very helpful. Thanks. I guess this functionality in the nightly build. If so, I will download and test again. Stay tuned.

comment:32 Changed 11 years ago by bill

Resolution: duplicate
Status: reopenedclosed

OK, I'm going to assume this is a dup of #5672. Seems like it is.

comment:33 Changed 11 years ago by bill

Milestone: 1.31.2
Note: See TracTickets for help on using tickets.