Merge lp:~methanal-developers/methanal/extend-tab-api into lp:methanal

Proposed by Jonathan Jacobs
Status: Merged
Approved by: Tristan Seligmann
Approved revision: 140
Merged at revision: 138
Proposed branch: lp:~methanal-developers/methanal/extend-tab-api
Merge into: lp:methanal
Diff against target: 1076 lines (+566/-166)
6 files modified
methanal/errors.py (+7/-0)
methanal/js/Methanal/Tests/TestWidgets.js (+17/-3)
methanal/js/Methanal/Widgets.js (+194/-69)
methanal/test/test_widgets.py (+138/-44)
methanal/themes/methanal-base/methanal-tab-view.html (+1/-1)
methanal/widgets.py (+209/-49)
To merge this branch: bzr merge lp:~methanal-developers/methanal/extend-tab-api
Reviewer Review Type Date Requested Status
Tristan Seligmann Approve
Review via email: mp+24080@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Tristan Seligmann (mithrandi) :
review: Approve
141. By Jonathan Jacobs

Rename private TabView method to be more accurate.

142. By Jonathan Jacobs

Fix test breakage.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'methanal/errors.py'
2--- methanal/errors.py 2009-09-17 16:36:09 +0000
3+++ methanal/errors.py 2010-05-23 13:10:50 +0000
4@@ -9,3 +9,10 @@
5 """
6 An invalid enumeration value was specified.
7 """
8+
9+
10+
11+class InvalidIdentifier(ValueError):
12+ """
13+ An invalid identifier was specified.
14+ """
15
16=== modified file 'methanal/js/Methanal/Tests/TestWidgets.js'
17--- methanal/js/Methanal/Tests/TestWidgets.js 2010-03-15 18:58:03 +0000
18+++ methanal/js/Methanal/Tests/TestWidgets.js 2010-05-23 13:10:50 +0000
19@@ -430,6 +430,7 @@
20 node, _tabIDs, _tabGroups, topLevel || false, _makeThrobber);
21 Methanal.Tests.Util.makeWidgetChildNode(tabView, 'div', 'throbber');
22 Methanal.Tests.Util.makeWidgetChildNode(tabView, 'ul', 'labels');
23+ Methanal.Tests.Util.makeWidgetChildNode(tabView, 'div', 'contents');
24 document.body.appendChild(node);
25 Methanal.Util.nodeInserted(tabView);
26 return tabView;
27@@ -449,6 +450,7 @@
28 'group': group || null})
29 tabView.addChildWidget(tab);
30 Methanal.Tests.Util.makeWidgetChildNode(tab, 'content');
31+ tabView.nodeById('contents').appendChild(tab.node);
32 Methanal.Util.nodeInserted(tab);
33 return tab;
34 },
35@@ -585,7 +587,7 @@
36 */
37 function test_groups(self) {
38 function checkGroup(group) {
39- var groupNode = tabView._groups[group.id];
40+ var groupNode = tabView._groups[group.id].content;
41 self.assertNotIdentical(groupNode, undefined);
42 self.assertIdentical(
43 groupNode.getElementsByTagName('li').length,
44@@ -614,7 +616,8 @@
45
46 /**
47 * Group visibility is determined by the visibility of the tabs it
48- * contains.
49+ * contains. Removing all the tabs from a group will result in that group
50+ * becoming invisible.
51 */
52 function test_groupVisibility(self) {
53 var group1 = Methanal.Widgets.TabGroup(
54@@ -627,7 +630,7 @@
55 var tab3 = self.createTab(
56 tabView, 'tab3', 'Tab 3', undefined, group1.id);
57
58- var node = tabView._groups[group1.id].parentNode;
59+ var node = tabView._groups[group1.id].content.parentNode;
60 tabView.hideTab(tab1);
61 self.assertNodeVisible(node);
62 tabView.hideTab(tab2);
63@@ -636,4 +639,15 @@
64 self.assertNodeHidden(node);
65 tabView.showTab(tab3);
66 self.assertNodeVisible(node);
67+
68+ // Stub out Nevow.Athena.Widget.detach.
69+ function detach() {}
70+
71+ tab2.detach = detach;
72+ tabView.removeTab(tab2);
73+ self.assertNodeVisible(node);
74+
75+ tab3.detach = detach;
76+ tabView.removeTab(tab3);
77+ self.assertNodeHidden(node);
78 });
79
80=== modified file 'methanal/js/Methanal/Widgets.js'
81--- methanal/js/Methanal/Widgets.js 2010-03-15 18:58:15 +0000
82+++ methanal/js/Methanal/Widgets.js 2010-05-23 13:10:50 +0000
83@@ -1440,25 +1440,158 @@
84
85
86 /**
87- * Client-side handler for appending a tabs from the server-side.
88- */
89- function _appendTabsFromServer(self, widgetInfos, tabGroups) {
90+ * Select the first visible tab.
91+ */
92+ function selectFirstVisibleTab(self) {
93+ try {
94+ self.selectTab(self.getFirstVisibleTab());
95+ } catch (e) {
96+ if (!(e instanceof Methanal.Widgets.UnknownTab)) {
97+ throw e;
98+ }
99+ }
100+ },
101+
102+
103+ /**
104+ * Client-side handler for updating tabs from the server-side.
105+ *
106+ * @param widgetInfos: New tab widgets to add.
107+ *
108+ * @type tabIDsToRemove: C{Array} of C{String}
109+ * @param tabIDsToRemove: Identifiers of L{methanal.widgets.Tab}s to remove.
110+ *
111+ * @param tabGroups: Tab groups.
112+ */
113+ function _updateTabsFromServer(self, widgetInfos, tabIDsToRemove, tabGroups) {
114 self.throbber.start();
115 self.tabGroups = tabGroups;
116
117- function _appendTab(widgetInfo) {
118+ function _updateTab(widgetInfo) {
119 var d = self.addChildWidgetFromWidgetInfo(widgetInfo);
120- d.addCallback(function (widget) {
121- self.tabIDs[widget.id] = true;
122- self.node.appendChild(widget.node);
123- Methanal.Util.nodeInserted(widget);
124+ d.addCallback(function (tab) {
125+ var oldTab = self._tabs[tab.id];
126+ // If we are updating an existing tab then detach and remove
127+ // the old one first. Selection state is transferred too.
128+ if (oldTab !== undefined) {
129+ tab.selected = oldTab.selected;
130+ self._removeTabContent(oldTab);
131+ }
132+ self.tabIDs[tab.id] = true;
133+ self.nodeById('contents').appendChild(tab.node);
134+ Methanal.Util.nodeInserted(tab);
135 return null;
136 });
137 return d;
138 }
139
140- var ds = Methanal.Util.map(_appendTab, widgetInfos);
141- return Divmod.Defer.gatherResults(ds);
142+ var d = Divmod.Defer.gatherResults(
143+ Methanal.Util.map(_updateTab, widgetInfos));
144+ if (tabIDsToRemove.length > 0) {
145+ d.addCallback(function () {
146+ return self._removeTabsFromServer(tabIDsToRemove, tabGroups);
147+ });
148+ }
149+ return d;
150+ },
151+
152+
153+ /**
154+ * Remove a tab.
155+ *
156+ * Group visibility is updated too, meaning any groups that contain no
157+ * visible tabs will be hidden.
158+ */
159+ function removeTab(self, tab) {
160+ var d = self._removeTabContent(tab);
161+ var label = self._labels[tab.id];
162+ label.parentNode.removeChild(label);
163+ delete self._labels[tab.id];
164+ delete self._tabs[tab.id];
165+ if (tab.group !== null) {
166+ self._updateGroupVisiblity(tab.group);
167+ }
168+ return d;
169+ },
170+
171+
172+ /**
173+ * Remove a tab widget's content.
174+ */
175+ function _removeTabContent(self, tab) {
176+ self.nodeById('contents').removeChild(tab.node);
177+ return tab.detach();
178+ },
179+
180+
181+ /**
182+ * Client-side handler for removing tabs from the server-side.
183+ *
184+ * @type tabIDs: C{Array} of C{String}
185+ * @param tabIDs: Identifiers of L{methanal.widgets.Tab}s to remove.
186+ *
187+ * @param tabGroups: Tab groups.
188+ */
189+ function _removeTabsFromServer(self, tabIDs, tabGroups) {
190+ self.tabGroups = tabGroups;
191+ var wasSelected = false;
192+
193+ function _removeTab(tabID) {
194+ var tab = self.getTab(tabID);
195+ wasSelected |= tab.selected;
196+ return self.removeTab(tab);
197+ }
198+
199+ var d = Divmod.Defer.gatherResults(
200+ Methanal.Util.map(_removeTab, tabIDs));
201+
202+ d.addCallback(function () {
203+ // If one of the removed tabs was selected then select another one.
204+ if (wasSelected) {
205+ self.selectFirstVisibleTab();
206+ }
207+ return null;
208+ });
209+ return d;
210+ },
211+
212+
213+ /**
214+ * Create or update a tab label.
215+ *
216+ * Tab group titles will be updated too.
217+ */
218+ function _createLabel(self, tab) {
219+ var D = Methanal.Util.DOMBuilder(self.node.ownerDocument);
220+ var label = D('li', {'class': 'methanal-tab-label'}, [
221+ D('a', {}, [tab.title])]);
222+ label.onclick = function onclick(node) {
223+ self.selectTab(tab);
224+ };
225+
226+ var labelClassName = tab.getLabelClassName();
227+ if (labelClassName) {
228+ Methanal.Util.addElementClass(label, labelClassName);
229+ }
230+
231+ var labelParent = self.nodeById('labels');
232+ if (tab.group !== null) {
233+ var nodes = self._getGroupNodes(tab.group);
234+ Methanal.Util.replaceNodeText(
235+ nodes.label, self.getGroup(tab.group).title);
236+ self._updateGroupVisiblity(tab.group);
237+ labelParent = nodes.content;
238+ }
239+
240+ var oldLabel = self._labels[tab.id];
241+ // If a label for this tab already exists, then update that label.
242+ if (oldLabel !== undefined) {
243+ oldLabel.parentNode.replaceChild(label, oldLabel);
244+ } else {
245+ labelParent.appendChild(label);
246+ }
247+
248+ self._labels[tab.id] = label;
249 },
250
251
252@@ -1470,22 +1603,24 @@
253 */
254 function _createGroupNode(self, id) {
255 var title = self.getGroup(id).title;
256-
257 var D = Methanal.Util.DOMBuilder(self.node.ownerDocument);
258- var group = D('ul', {'class': 'methanal-tab-group-tabs'}, []);
259- self._groups[id] = group;
260- var groupInner = D(
261+ var content = D('ul', {'class': 'methanal-tab-group-tabs'}, []);
262+ var label = D('div', {'class': 'methanal-tab-group-label'}, [title]);
263+ var inner = D(
264 'li', {'class': 'methanal-tab-label methanal-tab-group'}, [
265- D('div', {'class': 'methanal-tab-group-label'}, [title]),
266- group]);
267- self.nodeById('labels').appendChild(groupInner);
268+ label, content]);
269+ self._groups[id] = {
270+ 'content': content,
271+ 'label': label};
272+ self.nodeById('labels').appendChild(inner);
273 },
274
275
276 /**
277- * Get the DOM node for a group, creating it if it doesn't already exist.
278+ * Get the DOM nodes for a group's container and label, creating them if
279+ * they don't exist.
280 */
281- function _getGroupNode(self, id) {
282+ function _getGroupNodes(self, id) {
283 if (!(id in self._groups)) {
284 self._createGroupNode(id);
285 }
286@@ -1499,35 +1634,14 @@
287 * A new label is created for C{tab}, based on L{Tab.title}, and an
288 * C{onclick} handler attached.
289 */
290- function appendTab(self, tab) {
291- function onclick(node) {
292- self.selectTab(tab);
293- }
294-
295- var D = Methanal.Util.DOMBuilder(self.node.ownerDocument);
296- var label = D('li', {'class': 'methanal-tab-label'}, [
297- D('a', {}, [tab.title])]);
298- label.onclick = onclick;
299-
300- var labelClassName = tab.getLabelClassName();
301- if (labelClassName) {
302- Methanal.Util.addElementClass(label, labelClassName);
303- }
304-
305- self._labels[tab.id] = label;
306+ function updateTab(self, tab) {
307 self._tabs[tab.id] = tab;
308-
309- var labelParent = self.nodeById('labels');
310- if (tab.group !== null) {
311- labelParent = self._getGroupNode(tab.group);
312- self._updateGroupVisiblity(tab.group);
313- }
314- labelParent.appendChild(label);
315+ self._createLabel(tab);
316
317 // If this tab's identifier matches the one we're supposed to select,
318 // do it. Otherwise select this tab if its "selected" attribute is true
319 // and no tab selection has been made yet.
320- if (tab.id == self._idToSelect) {
321+ if (tab.id === self._idToSelect) {
322 self._tabToSelect = tab;
323 } else if (self._tabToSelect === undefined && tab.selected) {
324 self._tabToSelect = tab;
325@@ -1535,6 +1649,12 @@
326 },
327
328
329+ function appendTab(self, tab) {
330+ Divmod.msg('DEPRECATED: Use Methanal.Widgets.TabView.updateTab');
331+ return self.updateTab(tab);
332+ },
333+
334+
335 /**
336 * Select a tab.
337 *
338@@ -1559,6 +1679,9 @@
339 if (!self.fullyLoaded) {
340 self._tabToSelect = tab;
341 self._idToSelect = tab.id;
342+ } else {
343+ self._tabToSelect = undefined;
344+ self._idToSelect = undefined;
345 }
346 },
347
348@@ -1594,7 +1717,7 @@
349
350 // Change the visibility of the group parent node, not the inner
351 // container.
352- var groupNode = self._groups[groupID].parentNode;
353+ var groupNode = self._getGroupNodes(groupID).content.parentNode;
354 if (visible) {
355 Methanal.Util.removeElementClass(groupNode, 'hidden');
356 } else {
357@@ -1638,13 +1761,7 @@
358 tab.visible = false;
359 // If we're hiding the selected tab, select the first visible tab.
360 if (tab.selected) {
361- try {
362- self.selectTab(self.getFirstVisibleTab());
363- } catch (e) {
364- if (!(e instanceof Methanal.Widgets.UnknownTab)) {
365- throw e;
366- }
367- }
368+ self.selectFirstVisibleTab();
369 }
370
371 self._updateGroupVisiblity(tab.group);
372@@ -1658,7 +1775,7 @@
373 * finalised by calling L{_finishLoading}.
374 */
375 function loadedUp(self, tab) {
376- self.appendTab(tab);
377+ self.updateTab(tab);
378
379 delete self.tabIDs[tab.id];
380 for (var id in self.tabIDs) {
381@@ -1674,13 +1791,11 @@
382 */
383 function _finishLoading(self) {
384 self.throbber.stop();
385- if (self.fullyLoaded) {
386- return;
387- }
388-
389- self.fullyLoaded = true;
390- if (self._tabToSelect === undefined && self.childWidgets.length > 0) {
391- self._tabToSelect = self.childWidgets[0];
392+ if (!self.fullyLoaded) {
393+ self.fullyLoaded = true;
394+ if (self._tabToSelect === undefined && self.childWidgets.length > 0) {
395+ self._tabToSelect = self.childWidgets[0];
396+ }
397 }
398 if (self._tabToSelect) {
399 self.selectTab(self._tabToSelect);
400@@ -1780,6 +1895,25 @@
401
402
403 /**
404+ * Set an Athena widget as the tab content.
405+ */
406+ function _setContentFromWidgetInfo(self, widgetInfo, abortFetch) {
407+ var d = self.addChildWidgetFromWidgetInfo(widgetInfo);
408+ d.addCallback(function (widget) {
409+ if (abortFetch) {
410+ return widget.detach();
411+ } else {
412+ self._setContent(widget.node);
413+ self._currentWidget = widget;
414+ Methanal.Util.nodeInserted(widget);
415+ }
416+ return null;
417+ });
418+ return d;
419+ },
420+
421+
422+ /**
423 * Fetch the latest tab content from the server.
424 *
425 * If a fetch is already in progress it's result is discarded and a new
426@@ -1801,16 +1935,7 @@
427
428 var d = self.callRemote('getContent');
429 d.addCallback(function (widgetInfo) {
430- return self.addChildWidgetFromWidgetInfo(widgetInfo);
431- });
432- d.addCallback(function (widget) {
433- if (abortFetch) {
434- return widget.detach();
435- } else {
436- self._setContent(widget.node);
437- self._currentWidget = widget;
438- Methanal.Util.nodeInserted(widget);
439- }
440+ return self._setContentFromWidgetInfo(widgetInfo, abortFetch);
441 });
442 return d;
443 },
444
445=== modified file 'methanal/test/test_widgets.py'
446--- methanal/test/test_widgets.py 2010-03-16 22:01:24 +0000
447+++ methanal/test/test_widgets.py 2010-05-23 13:10:50 +0000
448@@ -5,7 +5,7 @@
449
450 from nevow import inevow
451
452-from methanal import widgets
453+from methanal import widgets, errors
454
455
456
457@@ -24,12 +24,12 @@
458
459 def test_create(self):
460 """
461- Creating a L{TabView} widget initialises L{TabView._tabIDs} and
462+ Creating a L{TabView} widget initialises L{TabView._tabsByID} and
463 L{TabView.tabs} with the values originally specified.
464 """
465 self.assertEquals(
466- self.tabView._tabIDs,
467- set([u'id1', u'id2', u'id3']))
468+ sorted(self.tabView._tabsByID.keys()),
469+ [u'id1', u'id2', u'id3'])
470 self.assertEquals(
471 self.tabView.tabs,
472 self.tabs)
473@@ -38,7 +38,7 @@
474 def test_createGroup(self):
475 """
476 Creating a L{methanal.widgets.TabView} widget initialises
477- L{methanal.widgets.TabView._tabIDs}, L{methanal.widgets.TabView.tabs}
478+ L{methanal.widgets.TabView._tabsByID}, L{methanal.widgets.TabView.tabs}
479 and L{methanal.widgets.TabView._tabGroups} with the values originally
480 specified, tab groups are merged in and managed.
481 """
482@@ -49,71 +49,117 @@
483 widgets.Tab(u'id5', u'Title 5', self.contentFactory)]
484 tabView = widgets.TabView(tabs)
485 self.assertEquals(
486- tabView._tabIDs,
487- set([u'id1', u'id2', u'id3', u'id4', u'id5']))
488+ tabView.getTabIDs(),
489+ [u'id1', u'id2', u'id3', u'id4', u'id5'])
490 self.assertEquals(
491 tabView._tabGroups,
492 {u'group1': tabGroup})
493
494
495- def test_appendDuplicateTab(self):
496- """
497- Appending a L{methanal.widgets.Tab} widget with an C{id} attribute that
498- matches one already being managed raises C{ValueError}.
499- """
500- self.assertRaises(ValueError,
501- self.tabView.appendTab, self.tabView.tabs[0])
502-
503-
504- def test_appendTabs(self):
505- """
506- Appending tabs on the server side manages them and invokes methods
507+ def test_updateTabs(self):
508+ """
509+ Updating tabs on the server side manages them and invokes methods
510 on the client side to insert them.
511 """
512 self.result = None
513
514 def callRemote(methodName, *a):
515- self.result = methodName == '_appendTabsFromServer'
516+ self.result = methodName == '_updateTabsFromServer'
517
518 tab = widgets.Tab(u'id4', u'Title 4', self.contentFactory)
519 self.patch(self.tabView, 'callRemote', callRemote)
520- self.tabView.appendTabs([tab])
521+ self.tabView.updateTabs([tab])
522 self.assertTrue(self.result)
523- self.assertIn(u'id4', self.tabView._tabIDs)
524+ self.assertNotIdentical(self.tabView.getTab(u'id4'), None)
525 self.assertIdentical(self.tabView.tabs[-1], tab)
526
527-
528- def test_appendDuplicateGroup(self):
529- """
530- Appending a L{methanal.widgets.TabGroup} widget with an C{id} attribute
531- that matches one already being managed raises C{ValueError}.
532- """
533- tabGroup = widgets.TabGroup(u'group1', u'Group', tabs=[
534- widgets.Tab(u'id4', u'Title 4', self.contentFactory)])
535- self.tabView._manageGroup(tabGroup)
536- self.assertRaises(ValueError,
537- self.tabView.appendGroup, tabGroup)
538-
539-
540- def test_appendGroup(self):
541- """
542- Appending a group on the server site manages it, and all the tabs it
543+ replacementTab = widgets.Tab(u'id1', u'New title', self.contentFactory)
544+ oldTab = self.tabView.getTab(u'id1')
545+ self.tabView.updateTabs([replacementTab])
546+ self.assertIdentical(self.tabView.getTab(u'id1'), replacementTab)
547+ self.assertNotIn(oldTab, self.tabView.tabs)
548+
549+
550+ def test_updateGroup(self):
551+ """
552+ Updating a group on the server site manages it, and all the tabs it
553 contains, and invokes methods on the client side to insert them.
554 """
555 self.result = None
556
557 def callRemote(methodName, *a):
558- self.result = methodName == '_appendTabsFromServer'
559+ self.result = methodName == '_updateTabsFromServer'
560
561 tab = widgets.Tab(u'id4', u'Title 4', self.contentFactory)
562 group = widgets.TabGroup(u'group1', u'Group', tabs=[tab])
563 self.patch(self.tabView, 'callRemote', callRemote)
564- self.tabView.appendGroup(group)
565+ self.tabView.updateGroup(group)
566 self.assertTrue(self.result)
567- self.assertIn(u'id4', self.tabView._tabIDs)
568- self.assertIn(u'group1', self.tabView._tabGroups)
569+ self.assertNotIdentical(self.tabView.getTab(u'id4'), None)
570+ self.assertNotIdentical(self.tabView.getGroup(u'group1'), None)
571 self.assertIdentical(self.tabView.tabs[-1], tab)
572
573+ # Update a group, and add a new tab.
574+ newTab = widgets.Tab(u'id5', u'Title 5', self.contentFactory)
575+ replacementGroup = widgets.TabGroup(
576+ u'group1', u'New Group', tabs=[newTab])
577+ self.tabView.updateGroup(replacementGroup)
578+ self.assertIdentical(
579+ self.tabView.getGroup(u'group1'), replacementGroup)
580+ self.assertNotIdentical(self.tabView.getTab(u'id5'), None)
581+ self.assertRaises(
582+ errors.InvalidIdentifier, self.tabView.getTab, u'id4')
583+ self.assertNotIn(tab, self.tabView.tabs)
584+
585+ # Remove a tab from a group.
586+ self.tabView.removeTabs([newTab])
587+ self.assertRaises(
588+ errors.InvalidIdentifier, self.tabView.getTab, u'id5')
589+ self.assertNotIn(newTab, self.tabView.getGroup(u'group1').tabs)
590+
591+
592+ def test_removeTabs(self):
593+ """
594+ Removing tabs on the server side releases them and invokes methods on
595+ the client side to remove them.
596+ """
597+ self.result = None
598+
599+ def callRemote(methodName, *a):
600+ self.result = methodName == '_removeTabsFromServer'
601+
602+ self.patch(self.tabView, 'callRemote', callRemote)
603+ tab = self.tabView.tabs[0]
604+ self.tabView.removeTabs([tab])
605+ self.assertTrue(self.result)
606+ self.assertRaises(
607+ errors.InvalidIdentifier, self.tabView.getTab, tab.id)
608+
609+
610+ def test_invalidRemove(self):
611+ """
612+ Trying to remove an unmanaged tab results in C{ValueError} being
613+ raised.
614+ """
615+ # Try a new tab.
616+ self.assertRaises(ValueError,
617+ self.tabView.removeTabs, [
618+ widgets.Tab(u'id99', u'Title 1', self.contentFactory)])
619+
620+ # Try dupe an ID.
621+ self.assertRaises(ValueError,
622+ self.tabView.removeTabs, [
623+ widgets.Tab(u'id1', u'Title 1', self.contentFactory)])
624+
625+
626+ def test_repr(self):
627+ """
628+ L{methanal.widgets.TabView} has an accurate string representation.
629+ """
630+ self.assertEquals(
631+ repr(self.tabView),
632+ "<TabView topLevel=False tabs=%r>" % self.tabs)
633+
634
635
636 class StaticTabTests(unittest.TestCase):
637@@ -140,6 +186,21 @@
638 self.assertEquals(tab.contentFactory(), content * 2)
639
640
641+ def test_repr(self):
642+ """
643+ L{methanal.widgets.Tab} (and by extension L{methanal.widgets.StaticTab})
644+ has an accurate string representation.
645+ """
646+ tab = widgets.StaticTab(
647+ id=u'id',
648+ title=u'Title',
649+ content=u'A content.')
650+ self.assertEquals(
651+ repr(tab),
652+ "<StaticTab id=u'id' title=u'Title' selected=False group=None>")
653+
654+
655+
656
657 class TabGroupTests(unittest.TestCase):
658 """
659@@ -152,7 +213,40 @@
660 tabs = [
661 widgets.Tab(u'id1', u'Title 1', None),
662 widgets.Tab(u'id2', u'Title 2', None)]
663- tabGroup = widgets.TabGroup(u'id', u'title', tabs=tabs)
664+ tabGroup = widgets.TabGroup(u'id', u'Title', tabs=tabs)
665 self.assertEquals(
666 inevow.IAthenaTransportable(tabGroup).getInitialArguments(),
667- [u'id', u'title', [u'id1', u'id2']])
668+ [u'id', u'Title', [u'id1', u'id2']])
669+
670+
671+ def test_mergeGroups(self):
672+ """
673+ L{methanal.widgets.TabGroup.mergeGroups} will merge two
674+ L{methanal.widgets.TabGroup}s together, preferring the metadata of the
675+ second specified group.
676+ """
677+ tabs = [
678+ widgets.Tab(u'id1', u'Title 1', None),
679+ widgets.Tab(u'id2', u'Title 2', None)]
680+ tabGroup1 = widgets.TabGroup(u'id', u'Title', tabs=tabs)
681+ tabs = [
682+ widgets.Tab(u'id3', u'Title 3', None)]
683+ tabGroup2 = widgets.TabGroup(u'id', u'Hello', tabs=tabs)
684+
685+ newGroup = widgets.TabGroup.mergeGroups(tabGroup1, tabGroup2)
686+ self.assertEquals(newGroup.id, u'id')
687+ self.assertEquals(newGroup.title, u'Hello')
688+ self.assertEquals(newGroup.tabs, tabGroup1.tabs + tabGroup2.tabs)
689+
690+
691+ def test_repr(self):
692+ """
693+ L{methanal.widgets.TabGroup} has an accurate string representation.
694+ """
695+ tabs = [
696+ widgets.Tab(u'id1', u'Title 1', None),
697+ widgets.Tab(u'id2', u'Title 2', None)]
698+ tabGroup = widgets.TabGroup(u'id', u'Title', tabs=tabs)
699+ self.assertEquals(
700+ repr(tabGroup),
701+ "<TabGroup id=u'id' title=u'Title' tabs=%r>" % tabs)
702
703=== modified file 'methanal/themes/methanal-base/methanal-tab-view.html'
704--- methanal/themes/methanal-base/methanal-tab-view.html 2010-01-03 21:23:44 +0000
705+++ methanal/themes/methanal-base/methanal-tab-view.html 2010-05-23 13:10:50 +0000
706@@ -2,6 +2,6 @@
707 <ul class="methanal-tab-labels" id="labels">
708 <li id="throbber" class="methanal-tab-label" style="padding: 0 2px;"><img src="/static/Methanal/images/main-throbber.gif" style="height: 10px; width: 10px;" /></li>
709 </ul>
710- <div style="clear: both;" nevow:render="tabContents" />
711+ <div id="contents" style="clear: both;" nevow:render="tabContents" />
712 <div class="terminator"></div>
713 </div>
714
715=== modified file 'methanal/widgets.py'
716--- methanal/widgets.py 2010-03-14 01:24:31 +0000
717+++ methanal/widgets.py 2010-05-23 13:10:50 +0000
718@@ -11,6 +11,8 @@
719
720 from twisted.internet.defer import maybeDeferred
721 from twisted.python.components import registerAdapter
722+from twisted.python.versions import Version
723+from twisted.python.deprecate import deprecated
724
725 from axiom.item import SQLAttribute
726
727@@ -28,6 +30,7 @@
728 ObjectSelectInput, SimpleForm, FormInput, LiveForm, SubmitAction,
729 ActionButton, ActionContainer)
730 from methanal.model import Value
731+from methanal.errors import InvalidIdentifier
732
733
734
735@@ -658,8 +661,8 @@
736 the fragment part of the current URL to track which tab is selected,
737 this behaviour supercedes L{Tab.selected}.
738
739- @type _tabIDs: C{set}
740- @ivar _tabIDs: Collection of unique tab IDs currently being managed.
741+ @type _tabsByID: C{dict} mapping C{unicode} to L{methanal.widgets.Tab}
742+ @ivar _tabsByID: Mapping of unique tab IDs to tabs currently being managed.
743
744 @type _tabGroups: C{dict} mapping C{unicode} to
745 L{methanal.widgets.TabGroup}
746@@ -675,7 +678,7 @@
747 @param tabs: Tab or tab groups to manage.
748 """
749 super(TabView, self).__init__(**kw)
750- self._tabIDs = set()
751+ self._tabsByID = {}
752 self._tabGroups = {}
753 self.tabs = []
754 self.topLevel = topLevel
755@@ -689,76 +692,170 @@
756 self._manageTab(tabOrGroup)
757
758
759+ def __repr__(self):
760+ return '<%s topLevel=%r tabs=%r>' % (
761+ type(self).__name__,
762+ self.topLevel,
763+ self.tabs)
764+
765+
766 def _manageGroup(self, group):
767 """
768 Begin managing a L{methanal.widgets.TabGroup}.
769
770- Tabs contained in a group are B{not} managed, L{_manageTab} should be
771- called for each tab.
772-
773- @raise ValueError: If the group's identifier matches one that is
774- already being managed.
775+ Tabs contained in a group are B{not} managed automatically,
776+ L{_manageTab} should be called for each tab.
777 """
778- if group.id in self._tabGroups:
779- raise ValueError('%r is a duplicate tab group identifier in %r' % (
780- group.id, self))
781 self._tabGroups[group.id] = group
782
783
784- def _manageTab(self, tab):
785- """
786- Begin managing a L{Tab} widget.
787-
788- @raise ValueError: If the tab's identifier matches one that is already
789- being managed.
790- """
791- if tab.id in self._tabIDs:
792+ def _manageTab(self, tab, overwrite=False):
793+ """
794+ Begin managing a L{methanal.widgets.Tab} widget.
795+
796+ If a tab identifier is already being managed it is released before
797+ managing the new widget.
798+ """
799+ if tab.id in self._tabsByID:
800+ self._releaseTab(self.getTab(tab.id))
801+ self._tabsByID[tab.id] = tab
802+ self.tabs.append(tab)
803+ group = self._tabGroups.get(tab.group)
804+ if group is not None:
805+ group._manageTab(tab)
806+
807+
808+ def _releaseTab(self, tab):
809+ """
810+ Stop managing a L{methanal.widgets.Tab} widget.
811+ """
812+ if tab not in self.tabs:
813 raise ValueError(
814- '%r is a duplicate tab identifier in %r' % (tab.id, self))
815- self._tabIDs.add(tab.id)
816- self.tabs.append(tab)
817-
818-
819+ '%r is not managed by %r' % (tab.id, self))
820+ del self._tabsByID[tab.id]
821+ self.tabs.remove(tab)
822+ group = self._tabGroups.get(tab.group)
823+ if group is not None:
824+ group._releaseTab(tab)
825+
826+
827+ def getTab(self, id):
828+ """
829+ Get a L{methanal.widgets.Tab} by its unique identifier.
830+ """
831+ tab = self._tabsByID.get(id)
832+ if tab is None:
833+ raise InvalidIdentifier(
834+ u'%r is not a valid tab identifier in %r' % (id, self))
835+ return tab
836+
837+
838+ def getTabIDs(self):
839+ """
840+ Get a C{list} of all the tab IDs in the group.
841+ """
842+ return [tab.id for tab in self.tabs]
843+
844+
845+ def getGroup(self, id):
846+ """
847+ Get a L{methanal.widgets.TabGroup} by its unique identifier.
848+ """
849+ group = self._tabGroups.get(id)
850+ if group is None:
851+ raise InvalidIdentifier(
852+ u'%r is not a valid group identifier in %r' % (id, self))
853+ return group
854+
855+
856+ @deprecated(Version('methanal', 0, 2, 1))
857 def appendTab(self, tab):
858 """
859- Append a L{Tab} widget.
860+ Append a L{methanal.widgets.Tab} widget.
861
862 @return: A C{Deferred} that fires when the widget has been inserted on
863 the client side.
864 """
865- return self.appendTabs([tab])
866-
867-
868- def appendTabs(self, tabs):
869+ return self.updateTabs([tab])
870+
871+
872+ def updateTabs(self, tabs, tabsToRemove=None):
873 """
874- Append many L{methanal.widgets.Tab} widgets.
875-
876- All tab widgets are passed to the client side and appended there, and
877- added to the relevant groups.
878-
879- @return: A C{Deferred} that fires when the widgets have been inserted on
880+ Update many L{methanal.widgets.Tab} widgets.
881+
882+ All tab widgets are passed to the client side, updated there (or
883+ appended if they didn't previously exist) and added to the relevant
884+ groups. Use L{methanal.widgets.TabGroup.mergeGroups} to simulate adding
885+ to an existing group.
886+
887+ @return: A C{Deferred} that fires when the widgets have been updated on
888 the client side.
889 """
890 for tab in tabs:
891 self._manageTab(tab)
892 tab.setFragmentParent(self)
893- return self.callRemote('_appendTabsFromServer', tabs, self._tabGroups)
894-
895-
896- def appendGroup(self, group):
897- """
898- Append a L{methanal.widgets.TabGroup} and its tabs.
899+
900+ tabIDsToRemove = []
901+ if tabsToRemove is not None:
902+ tabIDsToRemove = [tab.id for tab in tabsToRemove]
903+
904+ return self.callRemote(
905+ '_updateTabsFromServer', tabs, tabIDsToRemove, self._tabGroups)
906+
907+
908+ appendTabs = deprecated(Version('methanal', 0, 2, 1))(updateTabs)
909+
910+
911+ def removeTabs(self, tabs):
912+ """
913+ Remove many L{methanal.widgets.Tab} widgets.
914+
915+ Empty groups, caused by removing all contained tabs, are removed too.
916+
917+ @return: A C{Deferred} that fires when the widget has been removed on
918+ the client side.
919+ """
920+ tabIDs = []
921+ for tab in tabs:
922+ tabIDs.append(tab.id)
923+ self._releaseTab(tab)
924+
925+ return self.callRemote('_removeTabsFromServer', tabIDs, self._tabGroups)
926+
927+
928+ def updateGroup(self, group):
929+ """
930+ Update a L{methanal.widgets.TabGroup} and its tabs.
931+
932+ Tabs specified in C{group} will replace those previously specified by
933+ another group of the same name.
934
935 @return: A C{Deferred} that fires when the widgets have been inserted on
936 the client side.
937 """
938+ try:
939+ oldGroup = self.getGroup(group.id)
940+ except InvalidIdentifier:
941+ oldGroup = None
942+
943+ tabsToRemove = []
944+ if oldGroup is not None:
945+ tabsToRemove = map(
946+ self.getTab,
947+ set(oldGroup.getTabIDs()) - set(group.getTabIDs()))
948+ for tab in tabsToRemove:
949+ self._releaseTab(tab)
950+
951 self._manageGroup(group)
952- return self.appendTabs(group.tabs)
953+ return self.updateTabs(group.tabs, tabsToRemove)
954+
955+
956+ appendGroup = deprecated(Version('methanal', 0, 2, 1))(updateGroup)
957
958
959 def getInitialArguments(self):
960 return [
961- dict.fromkeys(self._tabIDs, True),
962+ dict.fromkeys(self._tabsByID.iterkeys(), True),
963 self._tabGroups,
964 self.topLevel]
965
966@@ -774,7 +871,7 @@
967
968
969
970-class TabGroup(record('id title tabs')):
971+class TabGroup(object):
972 """
973 Visually group labels of L{methanal.widgets.Tab}s together.
974
975@@ -792,15 +889,59 @@
976
977 jsClass = u'Methanal.Widgets.TabGroup'
978
979- def __init__(self, *a, **kw):
980- super(TabGroup, self).__init__(*a, **kw)
981- for tab in self.tabs:
982+ def __init__(self, id, title, tabs):
983+ self.id = id
984+ self.title = title
985+ self.tabs = []
986+ for tab in tabs:
987+ self._manageTab(tab)
988+
989+
990+ def __repr__(self):
991+ return '<%s id=%r title=%r tabs=%r>' % (
992+ type(self).__name__,
993+ self.id,
994+ self.title,
995+ self.tabs)
996+
997+
998+ def _manageTab(self, tab):
999+ """
1000+ Manage a L{methanal.widgets.Tab} widget.
1001+ """
1002+ if tab not in self.tabs:
1003+ self.tabs.append(tab)
1004 tab.group = self.id
1005
1006
1007+ def _releaseTab(self, tab):
1008+ """
1009+ Stop managing a L{methanal.widgets.Tab} widget.
1010+ """
1011+ if tab in self.tabs:
1012+ self.tabs.remove(tab)
1013+ tab.group = None
1014+
1015+
1016+ def getTabIDs(self):
1017+ """
1018+ Get a C{list} of all the tab IDs in the group.
1019+ """
1020+ return [tab.id for tab in self.tabs]
1021+
1022+
1023+ @classmethod
1024+ def mergeGroups(cls, old, new):
1025+ """
1026+ Merge two L{methanal.widgets.TabGroup}s together.
1027+ """
1028+ return cls(new.id, new.title, old.tabs + new.tabs)
1029+
1030+
1031+ # IAthenaTransportable
1032+
1033 def getInitialArguments(self):
1034- tabIDs = [tab.id for tab in self.tabs]
1035- return [self.id, self.title, tabIDs]
1036+ return [self.id, self.title, self.getTabIDs()]
1037
1038
1039
1040@@ -839,6 +980,15 @@
1041 self.group = group
1042
1043
1044+ def __repr__(self):
1045+ return '<%s id=%r title=%r selected=%r group=%r>' % (
1046+ type(self).__name__,
1047+ self.id,
1048+ self.title,
1049+ self.selected,
1050+ self.group)
1051+
1052+
1053 def getInitialArguments(self):
1054 return [getArgsDict(self)]
1055
1056@@ -887,6 +1037,10 @@
1057 return tag[self.getContent()]
1058
1059
1060+ def updateRemoteContent(self):
1061+ pass
1062+
1063+
1064
1065 class DynamicTab(Tab):
1066 """
1067@@ -909,3 +1063,9 @@
1068 being discarded and a new fetch occurring.
1069 """
1070 jsClass = u'Methanal.Widgets.DemandTab'
1071+
1072+ def updateRemoteContent(self):
1073+ """
1074+ Force the remote content to be updated.
1075+ """
1076+ return self.callRemote('_setContentFromWidgetInfo', self.getContent())

Subscribers

People subscribed via source and target branches