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.
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)
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.

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.

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.

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.

James Tunnicliffe (dooferlad) wrote :

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

Thanks!

review: Needs Resubmitting
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)
James Tunnicliffe (dooferlad) wrote :

Done.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'lib/lp/blueprints/javascript'
2=== added directory 'lib/lp/blueprints/javascript/tests'
3=== added file 'lib/lp/blueprints/javascript/tests/test_workitems.html'
4--- lib/lp/blueprints/javascript/tests/test_workitems.html 1970-01-01 00:00:00 +0000
5+++ lib/lp/blueprints/javascript/tests/test_workitems.html 2012-06-13 15:31:25 +0000
6@@ -0,0 +1,211 @@
7+<!DOCTYPE html>
8+<!--
9+Copyright 2012 Canonical Ltd. This software is licensed under the
10+GNU Affero General Public License version 3 (see the file LICENSE).
11+-->
12+
13+<html>
14+<head>
15+ <title>Test Work Item Expanders</title>
16+
17+ <!-- YUI and test setup -->
18+ <script type="text/javascript"
19+ src="../../../../../build/js/yui/yui/yui.js">
20+ </script>
21+ <link rel="stylesheet"
22+ href="../../../../../build/js/yui/console/assets/console-core.css" />
23+ <link rel="stylesheet"
24+ href="../../../../../build/js/yui/console/assets/skins/sam/console.css" />
25+ <link rel="stylesheet"
26+ href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
27+
28+ <script type="text/javascript"
29+ src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
30+
31+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
32+
33+ <!-- Dependencies -->
34+ <script type="text/javascript"
35+ src="../../../../../build/js/lp/app/effects/effects.js"></script>
36+ <script type="text/javascript"
37+ src="../../../../../build/js/lp/app/expander.js"></script>
38+
39+ <!-- The module under test. -->
40+ <script type="text/javascript" src="../workitems.js"></script>
41+
42+ <!-- The test suite. -->
43+ <script type="text/javascript" src="test_workitems.js"></script>
44+
45+</head>
46+<body class="yui3-skin-sam">
47+<ul id="suites">
48+ <li>lp.workitems.expanders</li>
49+</ul>
50+<!-- Example markup required by test suite -->
51+<div id="test-root">
52+ <div id="fixture"></div>
53+
54+ <script type="text/x-template" id="work-items-test-0">
55+ <div class="workitems-group" id="milestone_0">
56+ <table class="listing">
57+ <thead>
58+ <tr>
59+ <th>
60+ <div style="float: left; font-weight: normal;">All:
61+ <a href="#expandall" class="expandall_link"
62+ id="expand_milestone_0">Expand</a>
63+ <a href="#collapseall" class="collapseall_link"
64+ id="collapse_milestone_0">Collapse</a>
65+ <a href="#defaultall" class="defaultall_link"
66+ id="default_milestone_0">Default</a>
67+ </div>
68+ </th>
69+ </tr>
70+ </thead>
71+
72+ <tbody>
73+ <tr class="expandable">
74+ <td>
75+ <a href="#" class="expander">.</a>
76+ </td>
77+ </tr>
78+ </tbody>
79+
80+ <tbody class="collapsible-body default-collapsed">
81+ <tr class="padded">
82+ <td>
83+ Content.
84+ </td>
85+ </tr>
86+ </tbody>
87+
88+ <tbody>
89+ <tr class="expandable">
90+ <td>
91+ <a href="#" class="expander">.</a>
92+ </td>
93+ </tr>
94+ </tbody>
95+
96+ <tbody class="collapsible-body default-expanded">
97+ <tr class="padded">
98+ <td>
99+ Content.
100+ </td>
101+ </tr>
102+ </tbody>
103+
104+ </table>
105+ </div>
106+ </script>
107+
108+ <script type="text/x-template" id="work-items-test-default-collapsed">
109+ <div class="workitems-group" id="milestone_1">
110+ <table class="listing">
111+ <thead>
112+ <tr>
113+ <th>
114+ <div style="float: left; font-weight: normal;">All:
115+ <a href="#expandall" class="expandall_link"
116+ id="expand_milestone_1">Expand</a>
117+ <a href="#collapseall" class="collapseall_link"
118+ id="collapse_milestone_1">Collapse</a>
119+ <a href="#defaultall" class="defaultall_link"
120+ id="default_milestone_1">Default</a>
121+ </div>
122+ </th>
123+ </tr>
124+ </thead>
125+
126+ <tbody>
127+ <tr class="expandable">
128+ <td>
129+ <a href="#" class="expander">.</a>
130+ </td>
131+ </tr>
132+ </tbody>
133+
134+ <tbody class="collapsible-body default-collapsed">
135+ <tr class="padded">
136+ <td>
137+ Content.
138+ </td>
139+ </tr>
140+ </tbody>
141+
142+ <tbody>
143+ <tr class="expandable">
144+ <td>
145+ <a href="#" class="expander">.</a>
146+ </td>
147+ </tr>
148+ </tbody>
149+
150+ <tbody class="collapsible-body default-collapsed">
151+ <tr class="padded">
152+ <td>
153+ Content.
154+ </td>
155+ </tr>
156+ </tbody>
157+
158+ </table>
159+ </div>
160+ </script>
161+
162+ <script type="text/x-template" id="work-items-test-default-expanded">
163+ <div class="workitems-group" id="milestone_2">
164+ <table class="listing">
165+ <thead>
166+ <tr>
167+ <th>
168+ <div style="float: left; font-weight: normal;">All:
169+ <a href="#expandall" class="expandall_link"
170+ id="expand_milestone_2">Expand</a>
171+ <a href="#collapseall" class="collapseall_link"
172+ id="collapse_milestone_2">Collapse</a>
173+ <a href="#defaultall" class="defaultall_link"
174+ id="default_milestone_2">Default</a>
175+ </div>
176+ </th>
177+ </tr>
178+ </thead>
179+
180+ <tbody>
181+ <tr class="expandable">
182+ <td>
183+ <a href="#" class="expander">.</a>
184+ </td>
185+ </tr>
186+ </tbody>
187+
188+ <tbody class="collapsible-body default-expanded">
189+ <tr class="padded">
190+ <td>
191+ Content.
192+ </td>
193+ </tr>
194+ </tbody>
195+
196+ <tbody>
197+ <tr class="expandable">
198+ <td>
199+ <a href="#" class="expander">.</a>
200+ </td>
201+ </tr>
202+ </tbody>
203+
204+ <tbody class="collapsible-body default-expanded">
205+ <tr class="padded">
206+ <td>
207+ Content.
208+ </td>
209+ </tr>
210+ </tbody>
211+
212+ </table>
213+ </div>
214+ </script>
215+</div>
216+</body>
217+</html>
218
219=== added file 'lib/lp/blueprints/javascript/tests/test_workitems.js'
220--- lib/lp/blueprints/javascript/tests/test_workitems.js 1970-01-01 00:00:00 +0000
221+++ lib/lp/blueprints/javascript/tests/test_workitems.js 2012-06-13 15:31:25 +0000
222@@ -0,0 +1,153 @@
223+YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.picker',
224+ 'lp.workitems.expanders',
225+ 'event', 'node-event-simulate', 'dump', function(Y) {
226+
227+var suite = new Y.Test.Suite("lp.workitems.expanders Tests");
228+var module = Y.lp.workitems.expanders;
229+
230+suite.add(new Y.Test.Case({
231+ name: 'setUpWorkItemExpanders test',
232+
233+ setUp: function() {
234+ this.fixture = Y.one("#fixture");
235+ module.test__ping_called = false;
236+ },
237+
238+ tearDown: function () {
239+ if (this.fixture !== null) {
240+ this.fixture.empty();
241+ }
242+ delete this.fixture;
243+ delete module.test__ping_called;
244+ },
245+
246+ _setup_fixture: function(template_selector) {
247+ var template = Y.one(template_selector).getContent();
248+ var test_node = Y.Node.create(template);
249+ this.fixture.append(test_node);
250+ },
251+
252+ _all_expanders_are_closed: function(){
253+ var found_open = false;
254+ Y.all('.collapsible-body').each(function(e) {
255+ found_collapsible_body = true;
256+ if (!e.hasClass('unseen'))
257+ {
258+ found_open = true;
259+ return;
260+ }
261+ });
262+ return found_open === false;
263+ },
264+
265+ _all_expanders_are_open: function(){
266+ var found_closed = false;
267+ Y.all('.collapsible-body').each(function(e) {
268+ found_collapsible_body = true;
269+ if (e.hasClass('unseen'))
270+ {
271+ found_closed = true;
272+ return;
273+ }
274+ });
275+ return found_closed === false;
276+ },
277+
278+ test_setUpWorkItemExpanders: function() {
279+ this._setup_fixture('#work-items-test-0');
280+
281+ Y.all('.collapsible-body').each(function(e) {
282+ Y.Assert.isFalse(e.hasClass('lazr-closed'));
283+ });
284+
285+ Y.all('[class=expandable]').each(function(e) {
286+ module._add_expanders(e);
287+ });
288+
289+ Y.all('.collapsible-body').each(function(e) {
290+ // For some reason lazr-closed is attached when expander bodies are
291+ // in their default state. Once clicked open they get lazr-open
292+ // and clicked closed they get the classes lazr-closed and unseen.
293+ // This assert is more encapsulating in case this changes.
294+ Y.Assert.isTrue(e.hasClass('lazr-closed') ||
295+ e.hasClass('lazr-open'));
296+ });
297+ },
298+
299+ attach_ping_catcher: function(event){
300+ module._attach_handler(event, function(){
301+ module.test__ping_called = true;
302+ });
303+ },
304+
305+ test_attach_handler: function() {
306+ // Test that _attach_handler attaches a function as expected. This
307+ // covers expand, collapse and default functions.
308+ this._setup_fixture('#work-items-test-0');
309+
310+ // Call the expander attach handler function
311+ Y.all('.expandall_link').on("click", this.attach_ping_catcher);
312+
313+ // Check that it attached correctly
314+ Y.one('.expandall_link').simulate('click');
315+ Y.Assert.isTrue(module.test__ping_called);
316+ },
317+
318+ test_default_closed: function() {
319+ // Test that clicking the default link restores expanders to their
320+ // initial state.
321+
322+ this._setup_fixture('#work-items-test-default-collapsed');
323+ module.setUpWorkItemExpanders({ no_animation: true });
324+
325+ // The test document should have all expanders collapsed. Check this.
326+ Y.Assert.isTrue(this._all_expanders_are_closed());
327+
328+ // Expand everything
329+ Y.one('.expandall_link').simulate('click');
330+ Y.Assert.isTrue(this._all_expanders_are_open());
331+
332+ // Return to default (collapsed)
333+ Y.one('.defaultall_link').simulate('click');
334+ Y.Assert.isTrue(this._all_expanders_are_closed());
335+
336+ // Collapse everything (should be no change)
337+ Y.one('.collapseall_link').simulate('click');
338+ Y.Assert.isTrue(this._all_expanders_are_closed());
339+
340+ // Check default link leaves everything closed
341+ Y.one('.defaultall_link').simulate('click');
342+ Y.Assert.isTrue(this._all_expanders_are_closed());
343+ },
344+
345+ test_default_open: function() {
346+ // Test that clicking the default link restores expanders to their
347+ // initial state.
348+
349+ this._setup_fixture('#work-items-test-default-expanded');
350+ module.setUpWorkItemExpanders({ no_animation: true });
351+
352+ // The test document should have all expanders collapsed. Check this.
353+ Y.Assert.isTrue(this._all_expanders_are_closed());
354+
355+ // Expand everything
356+ Y.one('.expandall_link').simulate('click');
357+ Y.Assert.isTrue(this._all_expanders_are_open());
358+
359+ // Set to default (should be no change)
360+ Y.one('.defaultall_link').simulate('click');
361+ Y.Assert.isTrue(this._all_expanders_are_open());
362+
363+ // Collapse everything
364+ Y.one('.collapseall_link').simulate('click');
365+ Y.Assert.isTrue(this._all_expanders_are_closed());
366+
367+ // Check default link opens everything back up
368+ Y.one('.defaultall_link').simulate('click');
369+ Y.Assert.isTrue(this._all_expanders_are_open());
370+ }
371+}));
372+
373+Y.lp.testing.Runner.run(suite);
374+
375+});
376
377=== added file 'lib/lp/blueprints/javascript/workitems.js'
378--- lib/lp/blueprints/javascript/workitems.js 1970-01-01 00:00:00 +0000
379+++ lib/lp/blueprints/javascript/workitems.js 2012-06-13 15:31:25 +0000
380@@ -0,0 +1,94 @@
381+/* Copyright 2012 Canonical Ltd. This software is licensed under the
382+ * GNU Affero General Public License version 3 (see the file LICENSE).
383+ *
384+ * Setup for managing subscribers list for bugs.
385+ *
386+ * @module workitems
387+ * @submodule expanders
388+ */
389+
390+YUI.add('lp.workitems.expanders', function(Y) {
391+
392+ var namespace = Y.namespace('lp.workitems.expanders');
393+
394+ /**
395+ * Record of all expanders and their default state.
396+ */
397+ var expanders = [];
398+
399+ function expander_expand(expander, i){
400+ expander[0].render(true, false);
401+ }
402+ namespace._expander_expand = expander_expand;
403+
404+ function expander_collapse(expander, i){
405+ expander[0].render(false, false);
406+ }
407+ namespace._expander_collapse = expander_collapse;
408+
409+ function expander_restore_default_state(expander, i){
410+ expander[0].render(expander[1], false);
411+ }
412+ namespace._expander_restore_default_state = expander_restore_default_state;
413+
414+ /**
415+ * Attach an expander to each expandable in the page.
416+ */
417+ function setUpWorkItemExpanders(expander_config){
418+ Y.all('[class=expandable]').each(function(e) {
419+ add_expanders(e, expander_config);
420+ });
421+
422+ Y.all('.expandall_link').on("click", function(event){
423+ attach_handler(event, expander_expand);
424+ });
425+
426+ Y.all('.collapseall_link').on("click", function(event){
427+ attach_handler(event, expander_collapse);
428+ });
429+
430+ Y.all('.defaultall_link').on("click", function(event){
431+ attach_handler(event, expander_restore_default_state);
432+ });
433+ }
434+ namespace.setUpWorkItemExpanders = setUpWorkItemExpanders;
435+
436+ function add_expanders(e, expander_config){
437+ var expander_icon = e.one('[class=expander]');
438+ // Our parent's first sibling is the tbody we want to collapse.
439+ var widget_body = e.ancestor().next();
440+
441+ if (Y.Lang.isUndefined(expander_config))
442+ {
443+ expander_config = {};
444+ }
445+
446+ var expander = new Y.lp.app.widgets.expander.Expander(expander_icon,
447+ widget_body,
448+ expander_config);
449+ expander.setUp(true);
450+
451+ var index = e.ancestor('[class=workitems-group]').get('id');
452+
453+ // We record the expanders so we can reference them later
454+ // First we have an array indexed by each milestone
455+ if (!Y.Lang.isValue(expanders[index])){
456+ expanders[index] = [];
457+ }
458+
459+ // For each milestone, store an array containing the expander
460+ // object and the default state for it
461+ default_expanded = widget_body.hasClass('default-expanded');
462+ expanders[index].push(new Array(expander, default_expanded));
463+ }
464+ namespace._add_expanders = add_expanders;
465+
466+ function attach_handler(event, func){
467+ var index = event.currentTarget.get('id');
468+ index = index.match(/milestone_\d+/)[0];
469+ Y.Array.forEach(expanders[index], func);
470+ }
471+ namespace._attach_handler = attach_handler;
472+
473+}, "0.1", {"requires": ["lp.app.widgets.expander"]});
474+
475
476=== modified file 'lib/lp/registry/templates/person-upcomingwork.pt'
477--- lib/lp/registry/templates/person-upcomingwork.pt 2012-05-31 02:20:41 +0000
478+++ lib/lp/registry/templates/person-upcomingwork.pt 2012-06-13 15:31:25 +0000
479@@ -10,16 +10,9 @@
480 <head>
481 <tal:block metal:fill-slot="head_epilogue">
482 <script type="text/javascript">
483- LPJS.use('node', 'event', 'lp.app.widgets.expander', function(Y) {
484+ LPJS.use('node', 'event', 'lp.app.widgets.expander', 'lp.workitems.expanders', function(Y) {
485 Y.on('domready', function() {
486- Y.all('[class=expandable]').each(function(e) {
487- var expander_icon = e.one('[class=expander]');
488- // Our parent's first sibling is the tbody we want to collapse.
489- var widget_body = e.ancestor().next();
490- var expander = new Y.lp.app.widgets.expander.Expander(
491- expander_icon, widget_body);
492- expander.setUp(true);
493- })
494+ Y.lp.workitems.expanders.setUpWorkItemExpanders();
495 })
496 });
497 </script>
498@@ -38,7 +31,8 @@
499
500 <div metal:fill-slot="main">
501
502- <div tal:repeat="pair view/work_item_containers" class="workitems-group">
503+ <div tal:repeat="pair view/work_item_containers" class="workitems-group"
504+ tal:attributes="id string:milestone_${repeat/pair/index}">
505 <div tal:define="date python: pair[0]; containers python: pair[1]">
506 <h2>Work items due in <span tal:replace="date/fmt:date" /></h2>
507
508@@ -78,7 +72,17 @@
509 <table class="listing">
510 <thead>
511 <tr>
512- <th>Blueprint</th>
513+ <th>
514+ <div style="float: right; font-weight: normal;">All:
515+ <a href="#expandall" class="expandall_link"
516+ tal:attributes="id string:expand_milestone_${repeat/pair/index}">Expand</a>
517+ <a href="#collapseall" class="collapseall_link"
518+ tal:attributes="id string:collapse_milestone_${repeat/pair/index}">Collapse</a>
519+ <a href="#defaultall" class="defaultall_link"
520+ tal:attributes="id string:default_milestone_${repeat/pair/index}">Default</a>
521+ </div>
522+ Blueprint
523+ </th>
524 <th>Target</th>
525 <th>Assignee</th>
526 <th>Priority</th>
527@@ -111,13 +115,15 @@
528
529 <tal:conditional condition="container/has_incomplete_work">
530 <tal:block define="global upcoming_work_class_name string:expanded"/>
531+ <tal:block define="global expander_init_state string:default-expanded"/>
532 </tal:conditional>
533
534 <tal:conditional condition="not: container/has_incomplete_work">
535 <tal:block define="global upcoming_work_class_name string:"/>
536+ <tal:block define="global expander_init_state string:default-collapsed"/>
537 </tal:conditional>
538
539- <tbody tal:attributes="class string:collapsible-body ${upcoming_work_class_name}">
540+ <tbody tal:attributes="class string:collapsible-body ${upcoming_work_class_name} ${expander_init_state}">
541 <tr tal:repeat="workitem container/items" class="padded">
542 <td>
543 <span tal:condition="not: container/spec|nothing"