Merge lp:~nataliabidart/ubuntuone-control-panel/letmeremove into lp:ubuntuone-control-panel

Proposed by Natalia Bidart
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 189
Merged at revision: 188
Proposed branch: lp:~nataliabidart/ubuntuone-control-panel/letmeremove
Merge into: lp:ubuntuone-control-panel
Diff against target: 583 lines (+243/-131)
9 files modified
data/qt/device.ui (+8/-1)
data/qt/devices.ui (+69/-105)
ubuntuone/controlpanel/gui/qt/device.py (+36/-9)
ubuntuone/controlpanel/gui/qt/devices.py (+5/-1)
ubuntuone/controlpanel/gui/qt/loadingoverlay.py (+2/-1)
ubuntuone/controlpanel/gui/qt/tests/__init__.py (+3/-0)
ubuntuone/controlpanel/gui/qt/tests/test_device.py (+89/-4)
ubuntuone/controlpanel/gui/qt/tests/test_devices.py (+14/-2)
ubuntuone/controlpanel/gui/tests/__init__.py (+17/-8)
To merge this branch: bzr merge lp:~nataliabidart/ubuntuone-control-panel/letmeremove
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) Approve
Roberto Alsina (community) Approve
Review via email: mp+68444@code.launchpad.net

Commit message

- Local device can now be removed (LP: #810662).

To post a comment you must log in.
188. By Natalia Bidart

Added code to remove local device once removed.

189. By Natalia Bidart

Merged trunk in.

Revision history for this message
Roberto Alsina (ralsina) wrote :

+1

review: Approve
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Clean code, tests pass, works irl. Approved!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/qt/device.ui'
2--- data/qt/device.ui 2011-07-11 11:19:09 +0000
3+++ data/qt/device.ui 2011-07-19 19:17:30 +0000
4@@ -24,7 +24,7 @@
5 <item>
6 <widget class="QLabel" name="device_name_label">
7 <property name="text">
8- <string>Another device</string>
9+ <string>Local device</string>
10 </property>
11 </widget>
12 </item>
13@@ -41,6 +41,13 @@
14 </property>
15 </spacer>
16 </item>
17+ <item>
18+ <widget class="QPushButton" name="remove_device_button">
19+ <property name="text">
20+ <string>Delete device</string>
21+ </property>
22+ </widget>
23+ </item>
24 </layout>
25 </widget>
26 <resources>
27
28=== modified file 'data/qt/devices.ui'
29--- data/qt/devices.ui 2011-07-11 11:19:09 +0000
30+++ data/qt/devices.ui 2011-07-19 19:17:30 +0000
31@@ -6,8 +6,8 @@
32 <rect>
33 <x>0</x>
34 <y>0</y>
35- <width>688</width>
36- <height>371</height>
37+ <width>325</width>
38+ <height>252</height>
39 </rect>
40 </property>
41 <property name="windowTitle">
42@@ -21,111 +21,75 @@
43 <number>0</number>
44 </property>
45 <item>
46- <widget class="QFrame" name="frame">
47- <property name="frameShape">
48- <enum>QFrame::StyledPanel</enum>
49- </property>
50- <property name="frameShadow">
51- <enum>QFrame::Raised</enum>
52- </property>
53- <layout class="QVBoxLayout" name="verticalLayout_2">
54- <item>
55- <layout class="QHBoxLayout" name="horizontalLayout_2">
56- <item>
57- <layout class="QVBoxLayout" name="local_device_box"/>
58- </item>
59- <item>
60- <spacer name="horizontalSpacer_2">
61- <property name="orientation">
62- <enum>Qt::Horizontal</enum>
63- </property>
64- <property name="sizeHint" stdset="0">
65- <size>
66- <width>40</width>
67- <height>20</height>
68- </size>
69- </property>
70- </spacer>
71- </item>
72- <item>
73- <widget class="QPushButton" name="delete_device_button">
74- <property name="text">
75- <string>Delete device</string>
76- </property>
77- </widget>
78- </item>
79- </layout>
80- </item>
81- <item>
82- <layout class="QHBoxLayout" name="horizontalLayout">
83- <item>
84- <widget class="QLabel" name="other_devices_label">
85- <property name="text">
86- <string>Other devices</string>
87- </property>
88- </widget>
89- </item>
90- <item>
91- <spacer name="horizontalSpacer">
92- <property name="orientation">
93- <enum>Qt::Horizontal</enum>
94- </property>
95- <property name="sizeHint" stdset="0">
96- <size>
97- <width>40</width>
98- <height>20</height>
99- </size>
100- </property>
101- </spacer>
102- </item>
103- </layout>
104- </item>
105- <item>
106- <widget class="QListWidget" name="list_devices">
107- <property name="alternatingRowColors">
108- <bool>true</bool>
109- </property>
110- <property name="iconSize">
111- <size>
112- <width>32</width>
113- <height>32</height>
114- </size>
115- </property>
116- <property name="spacing">
117- <number>0</number>
118- </property>
119- <property name="selectionRectVisible">
120- <bool>false</bool>
121- </property>
122- </widget>
123- </item>
124- <item>
125- <layout class="QHBoxLayout" name="horizontalLayout_3">
126- <item>
127- <widget class="QPushButton" name="manage_devices_button">
128- <property name="sizePolicy">
129- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
130- <horstretch>0</horstretch>
131- <verstretch>0</verstretch>
132- </sizepolicy>
133- </property>
134- <property name="layoutDirection">
135- <enum>Qt::RightToLeft</enum>
136- </property>
137- <property name="text">
138- <string>Go to the web page to manage your other devices</string>
139- </property>
140- <property name="icon">
141- <iconset resource="images.qrc">
142- <normaloff>:/external_icon_white.png</normaloff>:/external_icon_white.png</iconset>
143- </property>
144- </widget>
145- </item>
146- </layout>
147- </item>
148- </layout>
149+ <layout class="QVBoxLayout" name="local_device_box"/>
150+ </item>
151+ <item>
152+ <layout class="QHBoxLayout" name="horizontalLayout">
153+ <item>
154+ <widget class="QLabel" name="other_devices_label">
155+ <property name="text">
156+ <string>Other devices</string>
157+ </property>
158+ </widget>
159+ </item>
160+ <item>
161+ <spacer name="horizontalSpacer">
162+ <property name="orientation">
163+ <enum>Qt::Horizontal</enum>
164+ </property>
165+ <property name="sizeHint" stdset="0">
166+ <size>
167+ <width>40</width>
168+ <height>20</height>
169+ </size>
170+ </property>
171+ </spacer>
172+ </item>
173+ </layout>
174+ </item>
175+ <item>
176+ <widget class="QListWidget" name="list_devices">
177+ <property name="alternatingRowColors">
178+ <bool>true</bool>
179+ </property>
180+ <property name="iconSize">
181+ <size>
182+ <width>32</width>
183+ <height>32</height>
184+ </size>
185+ </property>
186+ <property name="spacing">
187+ <number>0</number>
188+ </property>
189+ <property name="selectionRectVisible">
190+ <bool>false</bool>
191+ </property>
192 </widget>
193 </item>
194+ <item>
195+ <layout class="QHBoxLayout" name="horizontalLayout_3">
196+ <item>
197+ <widget class="QPushButton" name="manage_devices_button">
198+ <property name="sizePolicy">
199+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
200+ <horstretch>0</horstretch>
201+ <verstretch>0</verstretch>
202+ </sizepolicy>
203+ </property>
204+ <property name="layoutDirection">
205+ <enum>Qt::RightToLeft</enum>
206+ </property>
207+ <property name="text">
208+ <string>Go to the web page to manage your other devices</string>
209+ </property>
210+ <property name="icon">
211+ <iconset resource="images.qrc">
212+ <normaloff>:/external_icon_white.png</normaloff>:/external_icon_white.png</iconset>
213+ </property>
214+ </widget>
215+ </item>
216+ </layout>
217+ </item>
218 </layout>
219 </widget>
220 <resources>
221
222=== modified file 'ubuntuone/controlpanel/gui/qt/device.py'
223--- ubuntuone/controlpanel/gui/qt/device.py 2011-07-11 18:00:59 +0000
224+++ ubuntuone/controlpanel/gui/qt/device.py 2011-07-19 19:17:30 +0000
225@@ -18,22 +18,30 @@
226
227 """The user interface for the control panel for Ubuntu One."""
228
229-from PyQt4 import QtGui
230-
231+from PyQt4 import QtGui, QtCore
232+
233+from twisted.internet import defer
234+
235+from ubuntuone.controlpanel.backend import (
236+ DEVICE_TYPE_COMPUTER,
237+ DEVICE_TYPE_PHONE,
238+)
239+from ubuntuone.controlpanel.gui import DEVICE_CONFIRM_REMOVE
240 from ubuntuone.controlpanel.gui.qt.ui import device_ui
241
242 COMPUTER_ICON = "computer"
243 PHONE_ICON = "phone"
244 DEFAULT_ICON = COMPUTER_ICON
245
246-COMPUTER_TYPE = "Computer"
247-PHONE_TYPE = "Phone"
248-
249 DEVICE_TYPE_TO_ICON_MAP = {
250- COMPUTER_TYPE: COMPUTER_ICON,
251- PHONE_TYPE: PHONE_ICON,
252+ DEVICE_TYPE_COMPUTER: COMPUTER_ICON,
253+ DEVICE_TYPE_PHONE: PHONE_ICON,
254 }
255
256+CANCEL = QtGui.QMessageBox.Cancel
257+NO = QtGui.QMessageBox.No
258+YES = QtGui.QMessageBox.Yes
259+
260
261 def icon_name_from_type(device_type):
262 """Get the icon name for the device."""
263@@ -44,11 +52,16 @@
264 class DeviceWidget(QtGui.QWidget):
265 """The widget for each device in the control panel."""
266
267- def __init__(self, *args):
268+ removed = QtCore.pyqtSignal()
269+ removeCanceled = QtCore.pyqtSignal()
270+
271+ def __init__(self, backend, device_id, **kwargs):
272 """Initialize the UI of the widget."""
273- QtGui.QWidget.__init__(self, *args)
274+ QtGui.QWidget.__init__(self, **kwargs)
275 self.ui = device_ui.Ui_Form()
276 self.ui.setupUi(self)
277+ self.id = device_id
278+ self.backend = backend
279
280 def update_device_info(self, device_info):
281 """Update the device info."""
282@@ -57,6 +70,20 @@
283 pixmap = QtGui.QPixmap(pixmap_name)
284 self.ui.device_icon_label.setPixmap(pixmap)
285
286+ @defer.inlineCallbacks
287+ @QtCore.pyqtSlot()
288+ def on_remove_device_button_clicked(self):
289+ """The user wants to remove this device."""
290+ msg = DEVICE_CONFIRM_REMOVE
291+ buttons = YES | NO
292+ response = QtGui.QMessageBox.warning(self, '', msg, buttons, NO)
293+
294+ if response == YES:
295+ yield self.backend.remove_device(device_id=self.id)
296+ self.removed.emit()
297+ else:
298+ self.removeCanceled.emit()
299+
300
301 def get_device_for_list_widget(device_info):
302 """Return a QListWidgetItem representing a device with the proper info."""
303
304=== modified file 'ubuntuone/controlpanel/gui/qt/devices.py'
305--- ubuntuone/controlpanel/gui/qt/devices.py 2011-07-10 21:35:02 +0000
306+++ ubuntuone/controlpanel/gui/qt/devices.py 2011-07-19 19:17:30 +0000
307@@ -81,8 +81,12 @@
308
309 def update_local_device(self, device_info):
310 """Update the info for the local device."""
311- device_widget = device.DeviceWidget()
312+ device_widget = device.DeviceWidget(backend=self.backend,
313+ device_id=device_info['device_id'])
314 device_widget.update_device_info(device_info)
315+ f = lambda: self.clear_device_info(self.ui.local_device_box)
316+ device_widget.removed.connect(f)
317+
318 self.ui.local_device_box.addWidget(device_widget)
319
320 def create_remote_device(self, device_info):
321
322=== modified file 'ubuntuone/controlpanel/gui/qt/loadingoverlay.py'
323--- ubuntuone/controlpanel/gui/qt/loadingoverlay.py 2011-07-12 18:47:13 +0000
324+++ ubuntuone/controlpanel/gui/qt/loadingoverlay.py 2011-07-19 19:17:30 +0000
325@@ -62,7 +62,8 @@
326
327 def eventFilter(self, obj, event):
328 """Filter events from Frame content to draw the dot animation."""
329- if obj == self.ui.frm_box and event.type() == QtCore.QEvent.Paint:
330+ if getattr(self, 'ui', None) is not None and \
331+ obj == self.ui.frm_box and event.type() == QtCore.QEvent.Paint:
332 painter = QtGui.QPainter()
333 painter.begin(obj)
334 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
335
336=== modified file 'ubuntuone/controlpanel/gui/qt/tests/__init__.py'
337--- ubuntuone/controlpanel/gui/qt/tests/__init__.py 2011-07-13 18:45:43 +0000
338+++ ubuntuone/controlpanel/gui/qt/tests/__init__.py 2011-07-19 19:17:30 +0000
339@@ -40,6 +40,7 @@
340 "name": "desktop i5",
341 "is_local": False,
342 "configurable": False,
343+ "device_id": '1258-6854',
344 }
345
346 SAMPLE_PHONE_INFO = {
347@@ -47,6 +48,7 @@
348 "name": "nokia 1100",
349 "is_local": False,
350 "configurable": False,
351+ "device_id": '987456-2321',
352 }
353
354 SAMPLE_DEVICES_INFO = [
355@@ -55,6 +57,7 @@
356 "name": "toshiba laptop",
357 "is_local": True,
358 "configurable": False,
359+ "device_id": '0000',
360 },
361 SAMPLE_COMPUTER_INFO,
362 SAMPLE_PHONE_INFO,
363
364=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_device.py'
365--- ubuntuone/controlpanel/gui/qt/tests/test_device.py 2011-07-13 18:45:43 +0000
366+++ ubuntuone/controlpanel/gui/qt/tests/test_device.py 2011-07-19 19:17:30 +0000
367@@ -20,9 +20,20 @@
368
369 from PyQt4 import QtGui
370
371+from twisted.internet import defer
372+
373 from ubuntuone.controlpanel.gui.qt import device as gui
374-from ubuntuone.controlpanel.gui.qt.tests import (BaseTestCase,
375- SAMPLE_COMPUTER_INFO, SAMPLE_PHONE_INFO)
376+from ubuntuone.controlpanel.gui.qt.tests import (
377+ BaseTestCase,
378+ FakedConfirmDialog,
379+ FakedControlPanelBackend,
380+ SAMPLE_COMPUTER_INFO,
381+ SAMPLE_PHONE_INFO,
382+)
383+
384+# Access to a protected member
385+# Instance of 'ControlBackend' has no '_called' member
386+# pylint: disable=W0212, E1103
387
388
389 class DeviceWidgetTestCase(BaseTestCase):
390@@ -31,6 +42,17 @@
391 innerclass_ui = gui.device_ui
392 innerclass_name = "Ui_Form"
393 class_ui = gui.DeviceWidget
394+ backend = FakedControlPanelBackend()
395+ device_id = 'zaraza'
396+ kwargs = {'backend': backend, 'device_id': device_id}
397+
398+ def test_has_id(self):
399+ """The device as an id, None by default."""
400+ self.assertEqual(self.ui.id, self.device_id)
401+
402+ def test_has_backend(self):
403+ """The device as a backend, None by default."""
404+ self.assertIs(self.ui.backend, self.backend)
405
406 def test_update_device_info(self):
407 """The widget is updated with the info."""
408@@ -48,8 +70,8 @@
409
410 def test_icon_name_from_type(self):
411 """Get the right icon name for a device type."""
412- self.assertIconMatchesType(gui.COMPUTER_TYPE, gui.COMPUTER_ICON)
413- self.assertIconMatchesType(gui.PHONE_TYPE, gui.PHONE_ICON)
414+ self.assertIconMatchesType(gui.DEVICE_TYPE_COMPUTER, gui.COMPUTER_ICON)
415+ self.assertIconMatchesType(gui.DEVICE_TYPE_PHONE, gui.PHONE_ICON)
416 self.assertIconMatchesType("other random type", gui.COMPUTER_ICON)
417
418 def _test_update_device_info_sets_right_icon(self, info):
419@@ -77,3 +99,66 @@
420 info = SAMPLE_PHONE_INFO
421 item = gui.get_device_for_list_widget(info)
422 self.assertEqual(item.text(), info["name"])
423+
424+
425+class RemoveDeviceTestCase(DeviceWidgetTestCase):
426+ """The test suite for the device deletion."""
427+
428+ @defer.inlineCallbacks
429+ def setUp(self):
430+ yield super(RemoveDeviceTestCase, self).setUp()
431+ FakedConfirmDialog.response = gui.NO
432+ FakedConfirmDialog.args = None
433+ FakedConfirmDialog.kwargs = None
434+ self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
435+
436+ def test_remove_device_opens_confirmation_dialog(self):
437+ """A confirmation dialog is opened when user clicks 'delete device'."""
438+ self.ui.ui.remove_device_button.click()
439+
440+ msg = gui.DEVICE_CONFIRM_REMOVE
441+ buttons = gui.YES | gui.NO
442+ self.assertEqual(FakedConfirmDialog.args,
443+ (self.ui, '', msg, buttons, gui.NO))
444+ self.assertEqual(FakedConfirmDialog.kwargs, {})
445+
446+ def test_remove_device_does_not_remove_if_answer_is_no(self):
447+ """The device is not removed is answer is No."""
448+ FakedConfirmDialog.response = gui.NO
449+ self.ui.removed.connect(self._set_called)
450+ self.ui.ui.remove_device_button.click()
451+
452+ self.assertNotIn('remove_device', self.ui.backend._called)
453+ self.assertEqual(self._called, False)
454+
455+ def test_remove_device_does_remove_if_answer_is_yes(self):
456+ """The device is removed is answer is Yes."""
457+ FakedConfirmDialog.response = gui.YES
458+ self.ui.ui.remove_device_button.click()
459+
460+ self.assert_backend_called('remove_device', device_id=self.device_id)
461+
462+ @defer.inlineCallbacks
463+ def test_remove_device_emits_signal_when_removed(self):
464+ """The signal 'removed' is emitted when removed."""
465+ d = defer.Deferred()
466+
467+ def check(device_id):
468+ """Fire deferred when the device was removed."""
469+ d.callback(device_id)
470+
471+ FakedConfirmDialog.response = gui.YES
472+ self.ui.removed.connect(self._set_called)
473+ self.patch(self.ui.backend, 'remove_device', check)
474+ self.ui.ui.remove_device_button.click()
475+
476+ yield d
477+ self.assertEqual(self._called, ((), {}))
478+
479+ def test_remove_device_emits_signal_when_not_removed(self):
480+ """The signal 'removeCanceled' is emitted when user cancels removal."""
481+ FakedConfirmDialog.response = gui.NO
482+ self.ui.removeCanceled.connect(self._set_called)
483+ self.ui.ui.remove_device_button.click()
484+
485+ self.assertEqual(self._called, ((), {}))
486
487=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_devices.py'
488--- ubuntuone/controlpanel/gui/qt/tests/test_devices.py 2011-07-11 11:19:09 +0000
489+++ ubuntuone/controlpanel/gui/qt/tests/test_devices.py 2011-07-19 19:17:30 +0000
490@@ -22,6 +22,7 @@
491
492 from ubuntuone.controlpanel.gui.qt import devices as gui
493 from ubuntuone.controlpanel.gui.qt.tests import (
494+ FakedConfirmDialog,
495 SAMPLE_DEVICES_INFO,
496 )
497 from ubuntuone.controlpanel.gui.qt.tests.test_ubuntuonebin import (
498@@ -40,6 +41,7 @@
499 def setUp(self):
500 yield super(DevicesPanelTestCase, self).setUp()
501 self.ui.backend.next_result = SAMPLE_DEVICES_INFO
502+ self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
503
504 def test_is_processing_while_asking_info(self):
505 """The ui is processing while the contents are loaded."""
506@@ -76,9 +78,10 @@
507 local, remote = SAMPLE_DEVICES_INFO[0], SAMPLE_DEVICES_INFO[1:]
508
509 self.assertEqual(self.ui.ui.local_device_box.count(), 1)
510- local_device = self.ui.ui.local_device_box.itemAt(0)
511- self.assertEqual(local_device.widget().ui.device_name_label.text(),
512+ local_device = self.ui.ui.local_device_box.itemAt(0).widget()
513+ self.assertEqual(local_device.ui.device_name_label.text(),
514 local['name'])
515+ self.assertEqual(local_device.id, local['device_id'])
516
517 self.assertEqual(self.ui.ui.list_devices.count(),
518 len(remote))
519@@ -97,3 +100,12 @@
520 self.ui.ui.manage_devices_button.click()
521
522 self.assertEqual(self._called, ((gui.EDIT_DEVICES_LINK,), {}))
523+
524+ def test_remove_device_widget_after_removal(self):
525+ """When a device widget was deleted, remove it from the UI."""
526+ self.ui.process_info(SAMPLE_DEVICES_INFO)
527+
528+ local_device = self.ui.ui.local_device_box.itemAt(0).widget()
529+ local_device.removed.emit()
530+
531+ self.assertTrue(self.ui.ui.local_device_box.itemAt(0) is None)
532
533=== modified file 'ubuntuone/controlpanel/gui/tests/__init__.py'
534--- ubuntuone/controlpanel/gui/tests/__init__.py 2011-07-01 12:39:37 +0000
535+++ ubuntuone/controlpanel/gui/tests/__init__.py 2011-07-19 19:17:30 +0000
536@@ -21,7 +21,11 @@
537 import os
538
539 from ubuntuone.controlpanel import gui
540-from ubuntuone.controlpanel.backend import ControlBackend
541+from ubuntuone.controlpanel.backend import (
542+ ControlBackend,
543+ DEVICE_TYPE_COMPUTER,
544+ DEVICE_TYPE_PHONE,
545+)
546 from ubuntuone.controlpanel.tests import USER_HOME, ROOT_PATH
547
548 # Attribute 'yyy' defined outside __init__, access to a protected member
549@@ -95,22 +99,27 @@
550 FAKE_VOLUMES_MINIMAL_INFO = [(u'', u'147852369', [ROOT, MUSIC_FOLDER])]
551
552 FAKE_DEVICE_INFO = {
553- 'device_id': '1258-6854', 'device_name': 'Baz', 'device_type': 'Computer',
554+ 'device_id': '1258-6854', 'device_name': 'Baz',
555+ 'device_type': DEVICE_TYPE_COMPUTER,
556 'is_local': 'True', 'configurable': 'True', 'limit_bandwidth': 'True',
557 'max_upload_speed': '1000', 'max_download_speed': '72548',
558 'show_all_notifications': 'True',
559 }
560
561 FAKE_DEVICES_INFO = [
562- {'device_id': '0', 'name': 'Ubuntu One @ Foo', 'type': 'Computer',
563- 'is_local': '', 'configurable': ''},
564- {'device_id': '1', 'name': 'Ubuntu One @ Bar', 'type': 'Phone',
565- 'is_local': '', 'configurable': ''},
566- {'device_id': '2', 'name': 'Ubuntu One @ Z', 'type': 'Computer',
567+ {'device_id': '0', 'name': 'Ubuntu One @ Foo',
568+ 'type': DEVICE_TYPE_COMPUTER,
569+ 'is_local': '', 'configurable': ''},
570+ {'device_id': '1', 'name': 'Ubuntu One @ Bar',
571+ 'type': DEVICE_TYPE_PHONE,
572+ 'is_local': '', 'configurable': ''},
573+ {'device_id': '2', 'name': 'Ubuntu One @ Z',
574+ 'type': DEVICE_TYPE_COMPUTER,
575 'is_local': '', 'configurable': 'True', 'limit_bandwidth': '',
576 'max_upload_speed': '0', 'max_download_speed': '0',
577 'show_all_notifications': ''},
578- {'device_id': '1258-6854', 'name': 'Ubuntu One @ Baz', 'type': 'Computer',
579+ {'device_id': '1258-6854', 'name': 'Ubuntu One @ Baz',
580+ 'type': DEVICE_TYPE_COMPUTER,
581 'is_local': 'True', 'configurable': 'True', 'limit_bandwidth': 'True',
582 'max_upload_speed': '1000', 'max_download_speed': '72548',
583 'show_all_notifications': 'True'}, # local

Subscribers

People subscribed via source and target branches