Merge lp:~dooferlad/launchpad/upcomingwork-expand-all into lp:launchpad

Proposed by James Tunnicliffe
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15410
Proposed branch: lp:~dooferlad/launchpad/upcomingwork-expand-all
Merge into: lp:launchpad
Diff against target: 543 lines (+476/-12)
4 files modified
lib/lp/blueprints/javascript/tests/test_workitems.html (+211/-0)
lib/lp/blueprints/javascript/tests/test_workitems.js (+153/-0)
lib/lp/blueprints/javascript/workitems.js (+94/-0)
lib/lp/registry/templates/person-upcomingwork.pt (+18/-12)
To merge this branch: bzr merge lp:~dooferlad/launchpad/upcomingwork-expand-all
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
James Tunnicliffe (community) Needs Resubmitting
Review via email: mp+107083@code.launchpad.net

Commit message

Upcoming work page should have expand all links

Description of the change

-- Summary
Feature Request: Upcoming work page should have expand all links. I also added collapse all and set back to default links since these seemed like logical extensions of the same feature (and likely to be requested if they weren’t present).

-- Proposed fix
Add JavaScript links to upcoming work view to expand / collapse / restore all work item lists for a blueprint.

-- Pre-implementation notes
None.

-- Implementation details
person-upcomingwork.pt updated with three new JS functions, one per link type. While initializing expanders, each expander object and its default state is stored in a new array, expanders[blueprint index][work item list index][expander object, default state]; This array is used to look up the required expanders when one of the links is selected.

-- LOC Rationale
Added a few lines because of new functionality. These will be more than offset by:
https://code.launchpad.net/~danilo/launchpad/kill-feedback-requests/+merge/106119

-- Tests
None.

-- Demo and Q/A
1. In a dev instance run http://paste.ubuntu.com/992291/ to generate some work items
2. Visit https://launchpad.dev/~hwdb-team/+upcomingwork and select the new links on the right hand side of the Blueprint column. If feeling adventurous you can create a new milestone for one of the blueprints that is within the next 60 days, but on a different day to the existing milestone and that will show up as a separate table. Links should only modify one table.

-- Lint
None.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

This JavaScript belongs in a module and requires a YUI test to verify it works. The lint report of the module will catch errors that commonly break older browsers as well as pointing out style issue. JavaScript uses 4-space identation

Avoid
    var things = new Array();
Use
   var things = [];

The floated div must be before the text "Blueprint" to display consistently across browsers. There is leading white-space.

I think this template is creating non-unique ids. This breaks many browsers. A proper test would catch this.

review: Needs Fixing (code)
Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Thanks for the review.

I haven't started on tests yet, but I have moved the existing code around as requested (I hope), so feedback on that is welcome.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for extracting the code.

Consider using
    !Y.Lang.isArray(expanders[index])
for
    expanders[index] === undefined
The code needs an array. or just use
   !Y.Lang.isValue(expanders[index])
if you just want to be sure there is something.

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Hi,

I am not sure how best to proceed with testing. I have got a test JS and HTML set up with a bit of example layout in the HTML for the JS to search through. It is my intention to run setUpWorkItemExpanders and maybe mock Y.lp.app.widgets.expander so I can avoid loading it and test that expanders array looks correct after it has run.

Other tests would start using the same template but simulate clicks to test each on click function is run when expected.

So, to do this I would need to pass in Y.lp.app.widgets.expander as a parameter to setUpWorkItemExpanders. I wasn't going to bother testing the parameters to Expander(expander_icon, widget_body) since I think the contents of expanders will demonstrate that we have extracted the data needed from the DOM.

One odd thing is that the JS console at the moment (in Chrome) contains:

TestRunner: Testing began at Fri May 25 2012 15:53:01 GMT+0100 (BST).
TestRunner: Test suite "lp.workitems.expanders Tests" started.
TestRunner: Testing began at Fri May 25 2012 15:53:01 GMT+0100 (BST).
TestRunner: Test suite "yuitests1337957581177" started.
TestRunner: Test case "setUpWorkItemExpanders test" started.
TestRunner: Test suite "yuitests1337957581177" completed.
Passed:0 Failed:0 Total:0 (0 ignored)
TestRunner: Testing completed at Fri May 25 2012 15:53:01 GMT+0100 (BST).
Passed:0 Failed:0 Total:0 (0 ignored)
TestRunner: test_setUpWorkItemExpanders: failed.
Unexpected error: Cannot call method 'reversible_slide_out' of undefined
TestRunner: Test case "setUpWorkItemExpanders test" completed.
Passed:0 Failed:1 Total:1 (0 ignored)
TestRunner: Test suite "lp.workitems.expanders Tests" completed.
Passed:0 Failed:1 Total:1 (0 ignored)
TestRunner: Testing completed at Fri May 25 2012 15:53:03 GMT+0100 (BST).
Passed:0 Failed:1 Total:1 (0 ignored)

I don't know where the Test suite "yuitests<random number>" came from. Only they are visible in the browser based console. I have pushed my branch so you can have a look at it, if you want to.

Revision history for this message
Curtis Hovey (sinzui) wrote :

You can ignore the random numbers. YUI assigns a unique id to every node it creates.

The test-suite should only test the functions in the code you wrote. We want to verify that the correct function is wired to the correct element. Since the functions are anonymous, I imagine we want to test that the element has or does not have the "hidden" class.

According to the module you created, it only requires lp.app.widgets.expander, so all but the last dependency can be deleted from the html file.

The html file is not setup to be reset by the test. Take a look at
    lib/lp/code/javascript/tests/test_util.*
Which uses the 'type="text/x-template"' technique to create html fragments for tests. The setup and teardown in the test-suite updates the fixture node to ensure each test has pristine markup.

test_util.js is testing several setup functions that also do hide and show (but not expanders). You might find this test helpful because it illustrates how to verify a method was called during an event, then checks the final state of the element. I think this style of test could be adapted by passing in an example expander config.

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

I believe I have addressed the previous comments. Please re-review.

Thanks!

review: Needs Resubmitting
Revision history for this message
Curtis Hovey (sinzui) wrote :

Lines 256 and 269 are missing the space between the keyword and the predicate:
    if(
should be
    if (

This looks good. I can land this once you fix the space.

review: Approve (code)
Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Done.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'lib/lp/blueprints/javascript'
=== added directory 'lib/lp/blueprints/javascript/tests'
=== added file 'lib/lp/blueprints/javascript/tests/test_workitems.html'
--- lib/lp/blueprints/javascript/tests/test_workitems.html 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/javascript/tests/test_workitems.html 2012-06-13 15:31:25 +0000
@@ -0,0 +1,211 @@
1<!DOCTYPE html>
2<!--
3Copyright 2012 Canonical Ltd. This software is licensed under the
4GNU Affero General Public License version 3 (see the file LICENSE).
5-->
6
7<html>
8<head>
9 <title>Test Work Item Expanders</title>
10
11 <!-- YUI and test setup -->
12 <script type="text/javascript"
13 src="../../../../../build/js/yui/yui/yui.js">
14 </script>
15 <link rel="stylesheet"
16 href="../../../../../build/js/yui/console/assets/console-core.css" />
17 <link rel="stylesheet"
18 href="../../../../../build/js/yui/console/assets/skins/sam/console.css" />
19 <link rel="stylesheet"
20 href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
21
22 <script type="text/javascript"
23 src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
24
25 <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
26
27 <!-- Dependencies -->
28 <script type="text/javascript"
29 src="../../../../../build/js/lp/app/effects/effects.js"></script>
30 <script type="text/javascript"
31 src="../../../../../build/js/lp/app/expander.js"></script>
32
33 <!-- The module under test. -->
34 <script type="text/javascript" src="../workitems.js"></script>
35
36 <!-- The test suite. -->
37 <script type="text/javascript" src="test_workitems.js"></script>
38
39</head>
40<body class="yui3-skin-sam">
41<ul id="suites">
42 <li>lp.workitems.expanders</li>
43</ul>
44<!-- Example markup required by test suite -->
45<div id="test-root">
46 <div id="fixture"></div>
47
48 <script type="text/x-template" id="work-items-test-0">
49 <div class="workitems-group" id="milestone_0">
50 <table class="listing">
51 <thead>
52 <tr>
53 <th>
54 <div style="float: left; font-weight: normal;">All:
55 <a href="#expandall" class="expandall_link"
56 id="expand_milestone_0">Expand</a>
57 <a href="#collapseall" class="collapseall_link"
58 id="collapse_milestone_0">Collapse</a>
59 <a href="#defaultall" class="defaultall_link"
60 id="default_milestone_0">Default</a>
61 </div>
62 </th>
63 </tr>
64 </thead>
65
66 <tbody>
67 <tr class="expandable">
68 <td>
69 <a href="#" class="expander">.</a>
70 </td>
71 </tr>
72 </tbody>
73
74 <tbody class="collapsible-body default-collapsed">
75 <tr class="padded">
76 <td>
77 Content.
78 </td>
79 </tr>
80 </tbody>
81
82 <tbody>
83 <tr class="expandable">
84 <td>
85 <a href="#" class="expander">.</a>
86 </td>
87 </tr>
88 </tbody>
89
90 <tbody class="collapsible-body default-expanded">
91 <tr class="padded">
92 <td>
93 Content.
94 </td>
95 </tr>
96 </tbody>
97
98 </table>
99 </div>
100 </script>
101
102 <script type="text/x-template" id="work-items-test-default-collapsed">
103 <div class="workitems-group" id="milestone_1">
104 <table class="listing">
105 <thead>
106 <tr>
107 <th>
108 <div style="float: left; font-weight: normal;">All:
109 <a href="#expandall" class="expandall_link"
110 id="expand_milestone_1">Expand</a>
111 <a href="#collapseall" class="collapseall_link"
112 id="collapse_milestone_1">Collapse</a>
113 <a href="#defaultall" class="defaultall_link"
114 id="default_milestone_1">Default</a>
115 </div>
116 </th>
117 </tr>
118 </thead>
119
120 <tbody>
121 <tr class="expandable">
122 <td>
123 <a href="#" class="expander">.</a>
124 </td>
125 </tr>
126 </tbody>
127
128 <tbody class="collapsible-body default-collapsed">
129 <tr class="padded">
130 <td>
131 Content.
132 </td>
133 </tr>
134 </tbody>
135
136 <tbody>
137 <tr class="expandable">
138 <td>
139 <a href="#" class="expander">.</a>
140 </td>
141 </tr>
142 </tbody>
143
144 <tbody class="collapsible-body default-collapsed">
145 <tr class="padded">
146 <td>
147 Content.
148 </td>
149 </tr>
150 </tbody>
151
152 </table>
153 </div>
154 </script>
155
156 <script type="text/x-template" id="work-items-test-default-expanded">
157 <div class="workitems-group" id="milestone_2">
158 <table class="listing">
159 <thead>
160 <tr>
161 <th>
162 <div style="float: left; font-weight: normal;">All:
163 <a href="#expandall" class="expandall_link"
164 id="expand_milestone_2">Expand</a>
165 <a href="#collapseall" class="collapseall_link"
166 id="collapse_milestone_2">Collapse</a>
167 <a href="#defaultall" class="defaultall_link"
168 id="default_milestone_2">Default</a>
169 </div>
170 </th>
171 </tr>
172 </thead>
173
174 <tbody>
175 <tr class="expandable">
176 <td>
177 <a href="#" class="expander">.</a>
178 </td>
179 </tr>
180 </tbody>
181
182 <tbody class="collapsible-body default-expanded">
183 <tr class="padded">
184 <td>
185 Content.
186 </td>
187 </tr>
188 </tbody>
189
190 <tbody>
191 <tr class="expandable">
192 <td>
193 <a href="#" class="expander">.</a>
194 </td>
195 </tr>
196 </tbody>
197
198 <tbody class="collapsible-body default-expanded">
199 <tr class="padded">
200 <td>
201 Content.
202 </td>
203 </tr>
204 </tbody>
205
206 </table>
207 </div>
208 </script>
209</div>
210</body>
211</html>
0212
=== added file 'lib/lp/blueprints/javascript/tests/test_workitems.js'
--- lib/lp/blueprints/javascript/tests/test_workitems.js 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/javascript/tests/test_workitems.js 2012-06-13 15:31:25 +0000
@@ -0,0 +1,153 @@
1YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.picker',
2 'lp.workitems.expanders',
3 'event', 'node-event-simulate', 'dump', function(Y) {
4
5var suite = new Y.Test.Suite("lp.workitems.expanders Tests");
6var module = Y.lp.workitems.expanders;
7
8suite.add(new Y.Test.Case({
9 name: 'setUpWorkItemExpanders test',
10
11 setUp: function() {
12 this.fixture = Y.one("#fixture");
13 module.test__ping_called = false;
14 },
15
16 tearDown: function () {
17 if (this.fixture !== null) {
18 this.fixture.empty();
19 }
20 delete this.fixture;
21 delete module.test__ping_called;
22 },
23
24 _setup_fixture: function(template_selector) {
25 var template = Y.one(template_selector).getContent();
26 var test_node = Y.Node.create(template);
27 this.fixture.append(test_node);
28 },
29
30 _all_expanders_are_closed: function(){
31 var found_open = false;
32 Y.all('.collapsible-body').each(function(e) {
33 found_collapsible_body = true;
34 if (!e.hasClass('unseen'))
35 {
36 found_open = true;
37 return;
38 }
39 });
40 return found_open === false;
41 },
42
43 _all_expanders_are_open: function(){
44 var found_closed = false;
45 Y.all('.collapsible-body').each(function(e) {
46 found_collapsible_body = true;
47 if (e.hasClass('unseen'))
48 {
49 found_closed = true;
50 return;
51 }
52 });
53 return found_closed === false;
54 },
55
56 test_setUpWorkItemExpanders: function() {
57 this._setup_fixture('#work-items-test-0');
58
59 Y.all('.collapsible-body').each(function(e) {
60 Y.Assert.isFalse(e.hasClass('lazr-closed'));
61 });
62
63 Y.all('[class=expandable]').each(function(e) {
64 module._add_expanders(e);
65 });
66
67 Y.all('.collapsible-body').each(function(e) {
68 // For some reason lazr-closed is attached when expander bodies are
69 // in their default state. Once clicked open they get lazr-open
70 // and clicked closed they get the classes lazr-closed and unseen.
71 // This assert is more encapsulating in case this changes.
72 Y.Assert.isTrue(e.hasClass('lazr-closed') ||
73 e.hasClass('lazr-open'));
74 });
75 },
76
77 attach_ping_catcher: function(event){
78 module._attach_handler(event, function(){
79 module.test__ping_called = true;
80 });
81 },
82
83 test_attach_handler: function() {
84 // Test that _attach_handler attaches a function as expected. This
85 // covers expand, collapse and default functions.
86 this._setup_fixture('#work-items-test-0');
87
88 // Call the expander attach handler function
89 Y.all('.expandall_link').on("click", this.attach_ping_catcher);
90
91 // Check that it attached correctly
92 Y.one('.expandall_link').simulate('click');
93 Y.Assert.isTrue(module.test__ping_called);
94 },
95
96 test_default_closed: function() {
97 // Test that clicking the default link restores expanders to their
98 // initial state.
99
100 this._setup_fixture('#work-items-test-default-collapsed');
101 module.setUpWorkItemExpanders({ no_animation: true });
102
103 // The test document should have all expanders collapsed. Check this.
104 Y.Assert.isTrue(this._all_expanders_are_closed());
105
106 // Expand everything
107 Y.one('.expandall_link').simulate('click');
108 Y.Assert.isTrue(this._all_expanders_are_open());
109
110 // Return to default (collapsed)
111 Y.one('.defaultall_link').simulate('click');
112 Y.Assert.isTrue(this._all_expanders_are_closed());
113
114 // Collapse everything (should be no change)
115 Y.one('.collapseall_link').simulate('click');
116 Y.Assert.isTrue(this._all_expanders_are_closed());
117
118 // Check default link leaves everything closed
119 Y.one('.defaultall_link').simulate('click');
120 Y.Assert.isTrue(this._all_expanders_are_closed());
121 },
122
123 test_default_open: function() {
124 // Test that clicking the default link restores expanders to their
125 // initial state.
126
127 this._setup_fixture('#work-items-test-default-expanded');
128 module.setUpWorkItemExpanders({ no_animation: true });
129
130 // The test document should have all expanders collapsed. Check this.
131 Y.Assert.isTrue(this._all_expanders_are_closed());
132
133 // Expand everything
134 Y.one('.expandall_link').simulate('click');
135 Y.Assert.isTrue(this._all_expanders_are_open());
136
137 // Set to default (should be no change)
138 Y.one('.defaultall_link').simulate('click');
139 Y.Assert.isTrue(this._all_expanders_are_open());
140
141 // Collapse everything
142 Y.one('.collapseall_link').simulate('click');
143 Y.Assert.isTrue(this._all_expanders_are_closed());
144
145 // Check default link opens everything back up
146 Y.one('.defaultall_link').simulate('click');
147 Y.Assert.isTrue(this._all_expanders_are_open());
148 }
149}));
150
151Y.lp.testing.Runner.run(suite);
152
153});
0154
=== added file 'lib/lp/blueprints/javascript/workitems.js'
--- lib/lp/blueprints/javascript/workitems.js 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/javascript/workitems.js 2012-06-13 15:31:25 +0000
@@ -0,0 +1,94 @@
1/* Copyright 2012 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Setup for managing subscribers list for bugs.
5 *
6 * @module workitems
7 * @submodule expanders
8 */
9
10YUI.add('lp.workitems.expanders', function(Y) {
11
12 var namespace = Y.namespace('lp.workitems.expanders');
13
14 /**
15 * Record of all expanders and their default state.
16 */
17 var expanders = [];
18
19 function expander_expand(expander, i){
20 expander[0].render(true, false);
21 }
22 namespace._expander_expand = expander_expand;
23
24 function expander_collapse(expander, i){
25 expander[0].render(false, false);
26 }
27 namespace._expander_collapse = expander_collapse;
28
29 function expander_restore_default_state(expander, i){
30 expander[0].render(expander[1], false);
31 }
32 namespace._expander_restore_default_state = expander_restore_default_state;
33
34 /**
35 * Attach an expander to each expandable in the page.
36 */
37 function setUpWorkItemExpanders(expander_config){
38 Y.all('[class=expandable]').each(function(e) {
39 add_expanders(e, expander_config);
40 });
41
42 Y.all('.expandall_link').on("click", function(event){
43 attach_handler(event, expander_expand);
44 });
45
46 Y.all('.collapseall_link').on("click", function(event){
47 attach_handler(event, expander_collapse);
48 });
49
50 Y.all('.defaultall_link').on("click", function(event){
51 attach_handler(event, expander_restore_default_state);
52 });
53 }
54 namespace.setUpWorkItemExpanders = setUpWorkItemExpanders;
55
56 function add_expanders(e, expander_config){
57 var expander_icon = e.one('[class=expander]');
58 // Our parent's first sibling is the tbody we want to collapse.
59 var widget_body = e.ancestor().next();
60
61 if (Y.Lang.isUndefined(expander_config))
62 {
63 expander_config = {};
64 }
65
66 var expander = new Y.lp.app.widgets.expander.Expander(expander_icon,
67 widget_body,
68 expander_config);
69 expander.setUp(true);
70
71 var index = e.ancestor('[class=workitems-group]').get('id');
72
73 // We record the expanders so we can reference them later
74 // First we have an array indexed by each milestone
75 if (!Y.Lang.isValue(expanders[index])){
76 expanders[index] = [];
77 }
78
79 // For each milestone, store an array containing the expander
80 // object and the default state for it
81 default_expanded = widget_body.hasClass('default-expanded');
82 expanders[index].push(new Array(expander, default_expanded));
83 }
84 namespace._add_expanders = add_expanders;
85
86 function attach_handler(event, func){
87 var index = event.currentTarget.get('id');
88 index = index.match(/milestone_\d+/)[0];
89 Y.Array.forEach(expanders[index], func);
90 }
91 namespace._attach_handler = attach_handler;
92
93}, "0.1", {"requires": ["lp.app.widgets.expander"]});
94
095
=== modified file 'lib/lp/registry/templates/person-upcomingwork.pt'
--- lib/lp/registry/templates/person-upcomingwork.pt 2012-05-31 02:20:41 +0000
+++ lib/lp/registry/templates/person-upcomingwork.pt 2012-06-13 15:31:25 +0000
@@ -10,16 +10,9 @@
10<head>10<head>
11 <tal:block metal:fill-slot="head_epilogue">11 <tal:block metal:fill-slot="head_epilogue">
12 <script type="text/javascript">12 <script type="text/javascript">
13 LPJS.use('node', 'event', 'lp.app.widgets.expander', function(Y) {13 LPJS.use('node', 'event', 'lp.app.widgets.expander', 'lp.workitems.expanders', function(Y) {
14 Y.on('domready', function() {14 Y.on('domready', function() {
15 Y.all('[class=expandable]').each(function(e) {15 Y.lp.workitems.expanders.setUpWorkItemExpanders();
16 var expander_icon = e.one('[class=expander]');
17 // Our parent's first sibling is the tbody we want to collapse.
18 var widget_body = e.ancestor().next();
19 var expander = new Y.lp.app.widgets.expander.Expander(
20 expander_icon, widget_body);
21 expander.setUp(true);
22 })
23 })16 })
24 });17 });
25 </script>18 </script>
@@ -38,7 +31,8 @@
3831
39<div metal:fill-slot="main">32<div metal:fill-slot="main">
4033
41 <div tal:repeat="pair view/work_item_containers" class="workitems-group">34 <div tal:repeat="pair view/work_item_containers" class="workitems-group"
35 tal:attributes="id string:milestone_${repeat/pair/index}">
42 <div tal:define="date python: pair[0]; containers python: pair[1]">36 <div tal:define="date python: pair[0]; containers python: pair[1]">
43 <h2>Work items due in <span tal:replace="date/fmt:date" /></h2>37 <h2>Work items due in <span tal:replace="date/fmt:date" /></h2>
4438
@@ -78,7 +72,17 @@
78 <table class="listing">72 <table class="listing">
79 <thead>73 <thead>
80 <tr>74 <tr>
81 <th>Blueprint</th>75 <th>
76 <div style="float: right; font-weight: normal;">All:
77 <a href="#expandall" class="expandall_link"
78 tal:attributes="id string:expand_milestone_${repeat/pair/index}">Expand</a>
79 <a href="#collapseall" class="collapseall_link"
80 tal:attributes="id string:collapse_milestone_${repeat/pair/index}">Collapse</a>
81 <a href="#defaultall" class="defaultall_link"
82 tal:attributes="id string:default_milestone_${repeat/pair/index}">Default</a>
83 </div>
84 Blueprint
85 </th>
82 <th>Target</th>86 <th>Target</th>
83 <th>Assignee</th>87 <th>Assignee</th>
84 <th>Priority</th>88 <th>Priority</th>
@@ -111,13 +115,15 @@
111115
112 <tal:conditional condition="container/has_incomplete_work">116 <tal:conditional condition="container/has_incomplete_work">
113 <tal:block define="global upcoming_work_class_name string:expanded"/>117 <tal:block define="global upcoming_work_class_name string:expanded"/>
118 <tal:block define="global expander_init_state string:default-expanded"/>
114 </tal:conditional>119 </tal:conditional>
115120
116 <tal:conditional condition="not: container/has_incomplete_work">121 <tal:conditional condition="not: container/has_incomplete_work">
117 <tal:block define="global upcoming_work_class_name string:"/>122 <tal:block define="global upcoming_work_class_name string:"/>
123 <tal:block define="global expander_init_state string:default-collapsed"/>
118 </tal:conditional>124 </tal:conditional>
119125
120 <tbody tal:attributes="class string:collapsible-body ${upcoming_work_class_name}">126 <tbody tal:attributes="class string:collapsible-body ${upcoming_work_class_name} ${expander_init_state}">
121 <tr tal:repeat="workitem container/items" class="padded">127 <tr tal:repeat="workitem container/items" class="padded">
122 <td>128 <td>
123 <span tal:condition="not: container/spec|nothing"129 <span tal:condition="not: container/spec|nothing"