Merge lp:~nataliabidart/ubuntu/natty/ubuntuone-control-panel/ubuntuone-control-panel-0.1.0 into lp:ubuntu/natty/ubuntuone-control-panel

Proposed by Natalia Bidart
Status: Merged
Merged at revision: 5
Proposed branch: lp:~nataliabidart/ubuntu/natty/ubuntuone-control-panel/ubuntuone-control-panel-0.1.0
Merge into: lp:ubuntu/natty/ubuntuone-control-panel
Diff against target: 4864 lines (+2941/-512)
29 files modified
PKG-INFO (+1/-1)
bin/ubuntuone-control-panel-backend (+2/-0)
bin/ubuntuone-control-panel-gtk (+4/-3)
data/account.ui (+92/-51)
data/controlpanel.ui (+0/-17)
data/device.ui (+203/-0)
data/devices.ui (+31/-1)
data/folders.ui (+26/-1)
data/management.ui (+16/-1)
debian/changelog (+60/-0)
debian/control (+4/-4)
po/POTFILES.in (+0/-1)
setup.py (+1/-1)
ubuntuone/controlpanel/__init__.py (+1/-1)
ubuntuone/controlpanel/backend.py (+176/-22)
ubuntuone/controlpanel/dbus_client.py (+144/-18)
ubuntuone/controlpanel/dbus_service.py (+117/-33)
ubuntuone/controlpanel/gtk/gui.py (+437/-77)
ubuntuone/controlpanel/gtk/tests/test_gui.py (+771/-65)
ubuntuone/controlpanel/gtk/tests/test_widgets.py (+6/-13)
ubuntuone/controlpanel/gtk/widgets.py (+15/-8)
ubuntuone/controlpanel/integrationtests/__init__.py (+5/-0)
ubuntuone/controlpanel/integrationtests/test_dbus_client_sd.py (+210/-41)
ubuntuone/controlpanel/integrationtests/test_dbus_client_sso.py (+128/-80)
ubuntuone/controlpanel/integrationtests/test_dbus_service.py (+102/-28)
ubuntuone/controlpanel/logger.py (+24/-1)
ubuntuone/controlpanel/tests/test_backend.py (+358/-19)
ubuntuone/controlpanel/utils.py (+0/-20)
ubuntuone/controlpanel/webclient.py (+7/-5)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu/natty/ubuntuone-control-panel/ubuntuone-control-panel-0.1.0
Reviewer Review Type Date Requested Status
Michael Terry Approve
Review via email: mp+44468@code.launchpad.net

Description of the change

 * debian/control
    - depends on ubutuone-client >= 1.5.1
    - depends on ubuntu-sso-client >= 1.1.7

  * New upstream release.
    [ Natalia B. Bidart <email address hidden> ]
      * Updated list of messages to be shown on the overview panel (LP:
      #690379).
      * Implemented callback for failure when loading devices.
      * Added a spinner on every UbuntuOneBin to be shown while loading the
      panel content.
      * Devices can now be removed (LP: #691295).
      * Migrated dbus_client to use non-deprectaed SSO D-Bus API.
      * Added clear_credentials to dbus_client module.
      * Implemented devices tab (LP: #690649).
      * Maximun size is set using geometry hints (LP: #683164).
      * Added logging to dbus_service and backend.
      * Error signals now sent the object id when available (LP: #691292).
      * After machine was added, Folders page is shown (LP: #674459).*
      VolumesInfoError signal is now handled (LP: #690292).
      * VolumesInfoError signal is now handled (LP: #690292).
      * When FileSyncStatusError is received, no more DbusException messages
      are leaked to the end user (LP: #690305).
      * User can now subcribe and unsubscribe from folders (LP: #689646).
      * Dbus booleans are now those strings whose bool() defines its value. So,
      '' for False and any other non empty string for True (LP: #683619).
      * File sync status is retrieved and displayed in the right top corner
      (LP: #673670).
      * Adding handling for file sync disabled (only backend for now).
      * Management panel is not twined itself if CredentialsFound signal is
     received several times (LP: #683649).
    [ Rodney Dawes <email address hidden> ]
      * Default to None and initialize if None in code, instead of mutable
      defaults.

To post a comment you must log in.
Revision history for this message
Michael Terry (mterry) wrote :

Pushed, with slight debian/changelog typo corrections.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'PKG-INFO'
2--- PKG-INFO 2010-12-06 12:27:11 +0000
3+++ PKG-INFO 2010-12-22 14:37:52 +0000
4@@ -1,6 +1,6 @@
5 Metadata-Version: 1.1
6 Name: ubuntuone-control-panel
7-Version: 0.0.9
8+Version: 0.1.0
9 Summary: Ubuntu One Control Panel
10 Home-page: https://launchpad.net/ubuntuone-control-panel
11 Author: Natalia Bidart
12
13=== modified file 'bin/ubuntuone-control-panel-backend'
14--- bin/ubuntuone-control-panel-backend 2010-12-06 12:27:11 +0000
15+++ bin/ubuntuone-control-panel-backend 2010-12-22 14:37:52 +0000
16@@ -18,6 +18,8 @@
17 # with this program. If not, see <http://www.gnu.org/licenses/>.
18 """Execute the DBus backend for the Ubuntu One control panel."""
19
20+# Invalid name "ubuntuone-control-panel-backend", pylint: disable=C0103
21+
22
23 from ubuntuone.controlpanel import dbus_service
24
25
26=== modified file 'bin/ubuntuone-control-panel-gtk'
27--- bin/ubuntuone-control-panel-gtk 2010-12-06 12:27:11 +0000
28+++ bin/ubuntuone-control-panel-gtk 2010-12-22 14:37:52 +0000
29@@ -18,7 +18,7 @@
30 # with this program. If not, see <http://www.gnu.org/licenses/>.
31 """Execute the graphical interface for the Ubuntu One control panel."""
32
33-import gtk
34+# Invalid name "ubuntuone-control-panel-gtk", pylint: disable=C0103
35
36 import dbus.mainloop.glib
37
38@@ -26,6 +26,7 @@
39
40 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
41
42+
43 if __name__ == "__main__":
44- ui = ubuntuone.controlpanel.gtk.gui.ControlPanelWindow()
45- ui.main()
46+ gui = ubuntuone.controlpanel.gtk.gui.ControlPanelWindow()
47+ gui.main()
48
49=== modified file 'data/account.ui'
50--- data/account.ui 2010-12-06 12:27:11 +0000
51+++ data/account.ui 2010-12-22 14:37:52 +0000
52@@ -9,26 +9,107 @@
53 <child>
54 <object class="GtkHBox" id="hbox1">
55 <property name="visible">True</property>
56- <property name="spacing">15</property>
57+ <property name="spacing">10</property>
58 <child>
59- <object class="GtkVBox" id="vbox1">
60+ <object class="GtkVBox" id="account">
61 <property name="visible">True</property>
62 <property name="spacing">10</property>
63 <child>
64- <object class="GtkHBox" id="name_box">
65+ <object class="GtkVBox" id="data">
66 <property name="visible">True</property>
67- <property name="spacing">5</property>
68+ <property name="spacing">10</property>
69 <child>
70- <object class="GtkLabel" id="name_label">
71+ <object class="GtkVBox" id="vbox1">
72 <property name="visible">True</property>
73- <property name="xalign">0</property>
74- <property name="label">Name:</property>
75+ <child>
76+ <object class="GtkLabel" id="label1">
77+ <property name="visible">True</property>
78+ <property name="xalign">0</property>
79+ <property name="label" translatable="yes">&lt;b&gt;Name:&lt;/b&gt;</property>
80+ <property name="use_markup">True</property>
81+ </object>
82+ <packing>
83+ <property name="position">0</property>
84+ </packing>
85+ </child>
86+ <child>
87+ <object class="GtkLabel" id="name_label">
88+ <property name="visible">True</property>
89+ <property name="xalign">0</property>
90+ <property name="xpad">12</property>
91+ <property name="label">tester name</property>
92+ <property name="use_markup">True</property>
93+ </object>
94+ <packing>
95+ <property name="expand">False</property>
96+ <property name="position">1</property>
97+ </packing>
98+ </child>
99 </object>
100 <packing>
101- <property name="expand">False</property>
102 <property name="position">0</property>
103 </packing>
104 </child>
105+ <child>
106+ <object class="GtkVBox" id="vbox2">
107+ <property name="visible">True</property>
108+ <child>
109+ <object class="GtkLabel" id="label3">
110+ <property name="visible">True</property>
111+ <property name="xalign">0</property>
112+ <property name="label" translatable="yes">&lt;b&gt;Account type:&lt;/b&gt;</property>
113+ <property name="use_markup">True</property>
114+ </object>
115+ <packing>
116+ <property name="position">0</property>
117+ </packing>
118+ </child>
119+ <child>
120+ <object class="GtkLabel" id="type_label">
121+ <property name="visible">True</property>
122+ <property name="xalign">0</property>
123+ <property name="xpad">12</property>
124+ <property name="label">22GB awesomeness</property>
125+ </object>
126+ <packing>
127+ <property name="position">1</property>
128+ </packing>
129+ </child>
130+ </object>
131+ <packing>
132+ <property name="position">1</property>
133+ </packing>
134+ </child>
135+ <child>
136+ <object class="GtkVBox" id="vbox3">
137+ <property name="visible">True</property>
138+ <child>
139+ <object class="GtkLabel" id="label2">
140+ <property name="visible">True</property>
141+ <property name="xalign">0</property>
142+ <property name="label" translatable="yes">&lt;b&gt;Email address:&lt;/b&gt;</property>
143+ <property name="use_markup">True</property>
144+ </object>
145+ <packing>
146+ <property name="position">0</property>
147+ </packing>
148+ </child>
149+ <child>
150+ <object class="GtkLabel" id="email_label">
151+ <property name="visible">True</property>
152+ <property name="xalign">0</property>
153+ <property name="xpad">12</property>
154+ <property name="label">a@example.com</property>
155+ </object>
156+ <packing>
157+ <property name="position">1</property>
158+ </packing>
159+ </child>
160+ </object>
161+ <packing>
162+ <property name="position">2</property>
163+ </packing>
164+ </child>
165 </object>
166 <packing>
167 <property name="expand">False</property>
168@@ -36,48 +117,6 @@
169 </packing>
170 </child>
171 <child>
172- <object class="GtkHBox" id="type_box">
173- <property name="visible">True</property>
174- <property name="spacing">5</property>
175- <child>
176- <object class="GtkLabel" id="label2">
177- <property name="visible">True</property>
178- <property name="xalign">0</property>
179- <property name="label" translatable="yes">Account type:</property>
180- </object>
181- <packing>
182- <property name="expand">False</property>
183- <property name="position">0</property>
184- </packing>
185- </child>
186- </object>
187- <packing>
188- <property name="expand">False</property>
189- <property name="position">1</property>
190- </packing>
191- </child>
192- <child>
193- <object class="GtkHBox" id="email_box">
194- <property name="visible">True</property>
195- <property name="spacing">5</property>
196- <child>
197- <object class="GtkLabel" id="label3">
198- <property name="visible">True</property>
199- <property name="xalign">0</property>
200- <property name="label" translatable="yes">Email address:</property>
201- </object>
202- <packing>
203- <property name="expand">False</property>
204- <property name="position">0</property>
205- </packing>
206- </child>
207- </object>
208- <packing>
209- <property name="expand">False</property>
210- <property name="position">2</property>
211- </packing>
212- </child>
213- <child>
214 <object class="GtkHButtonBox" id="hbuttonbox1">
215 <property name="layout_style">center</property>
216 <child>
217@@ -97,7 +136,7 @@
218 <packing>
219 <property name="expand">False</property>
220 <property name="pack_type">end</property>
221- <property name="position">3</property>
222+ <property name="position">1</property>
223 </packing>
224 </child>
225 </object>
226@@ -108,6 +147,7 @@
227 <child>
228 <object class="GtkVButtonBox" id="vbuttonbox1">
229 <property name="visible">True</property>
230+ <property name="spacing">10</property>
231 <property name="layout_style">center</property>
232 <child>
233 <object class="GtkLinkButton" id="linkbutton1">
234@@ -144,6 +184,7 @@
235 </object>
236 <packing>
237 <property name="expand">False</property>
238+ <property name="pack_type">end</property>
239 <property name="position">1</property>
240 </packing>
241 </child>
242
243=== removed file 'data/controlpanel.ui'
244--- data/controlpanel.ui 2010-12-06 12:27:11 +0000
245+++ data/controlpanel.ui 1970-01-01 00:00:00 +0000
246@@ -1,17 +0,0 @@
247-<?xml version="1.0" encoding="UTF-8"?>
248-<interface>
249- <requires lib="gtk+" version="2.16"/>
250- <!-- interface-naming-policy project-wide -->
251- <object class="GtkVBox" id="itself">
252- <property name="visible">True</property>
253- <child>
254- <placeholder/>
255- </child>
256- <child>
257- <placeholder/>
258- </child>
259- <child>
260- <placeholder/>
261- </child>
262- </object>
263-</interface>
264
265=== added file 'data/device.ui'
266--- data/device.ui 1970-01-01 00:00:00 +0000
267+++ data/device.ui 2010-12-22 14:37:52 +0000
268@@ -0,0 +1,203 @@
269+<?xml version="1.0" encoding="UTF-8"?>
270+<interface>
271+ <requires lib="gtk+" version="2.16"/>
272+ <!-- interface-naming-policy project-wide -->
273+ <object class="GtkAdjustment" id="adjustment2">
274+ <property name="upper">10000</property>
275+ <property name="step_increment">1</property>
276+ </object>
277+ <object class="GtkAdjustment" id="adjustment1">
278+ <property name="upper">10000</property>
279+ <property name="step_increment">1</property>
280+ </object>
281+ <object class="GtkVBox" id="itself">
282+ <property name="visible">True</property>
283+ <property name="spacing">5</property>
284+ <child>
285+ <object class="GtkHBox" id="hbox1">
286+ <property name="visible">True</property>
287+ <property name="spacing">15</property>
288+ <child>
289+ <object class="GtkVBox" id="vbox1">
290+ <property name="visible">True</property>
291+ <property name="spacing">5</property>
292+ <child>
293+ <object class="GtkHBox" id="hbox2">
294+ <property name="visible">True</property>
295+ <child>
296+ <object class="GtkImage" id="device_type">
297+ <property name="visible">True</property>
298+ <property name="icon_name">computer</property>
299+ </object>
300+ <packing>
301+ <property name="expand">False</property>
302+ <property name="position">0</property>
303+ </packing>
304+ </child>
305+ <child>
306+ <object class="GtkLabel" id="device_name">
307+ <property name="visible">True</property>
308+ <property name="xalign">0</property>
309+ <property name="xpad">5</property>
310+ <property name="label">My Laptop</property>
311+ </object>
312+ <packing>
313+ <property name="position">1</property>
314+ </packing>
315+ </child>
316+ <child>
317+ <object class="GtkLabel" id="device_id">
318+ <property name="label" translatable="yes">device id (hidden)</property>
319+ </object>
320+ <packing>
321+ <property name="expand">False</property>
322+ <property name="position">2</property>
323+ </packing>
324+ </child>
325+ <child>
326+ <object class="GtkLabel" id="date_added">
327+ <property name="label">30.02.09</property>
328+ </object>
329+ <packing>
330+ <property name="expand">False</property>
331+ <property name="position">3</property>
332+ </packing>
333+ </child>
334+ </object>
335+ <packing>
336+ <property name="position">0</property>
337+ </packing>
338+ </child>
339+ <child>
340+ <object class="GtkTable" id="throttling">
341+ <property name="visible">True</property>
342+ <property name="n_rows">3</property>
343+ <property name="n_columns">2</property>
344+ <property name="column_spacing">3</property>
345+ <property name="row_spacing">3</property>
346+ <child>
347+ <object class="GtkCheckButton" id="limit_bandwidth">
348+ <property name="label" translatable="yes">Limit bandwidth usage</property>
349+ <property name="visible">True</property>
350+ <property name="can_focus">True</property>
351+ <property name="receives_default">False</property>
352+ <property name="draw_indicator">True</property>
353+ <signal name="toggled" handler="on_limit_bandwidth_toggled" swapped="no"/>
354+ </object>
355+ </child>
356+ <child>
357+ <object class="GtkLabel" id="max_upload_speed_label">
358+ <property name="visible">True</property>
359+ <property name="xalign">1</property>
360+ <property name="xpad">5</property>
361+ <property name="label" translatable="yes">Max upload speed (KiB/s)</property>
362+ </object>
363+ <packing>
364+ <property name="top_attach">1</property>
365+ <property name="bottom_attach">2</property>
366+ </packing>
367+ </child>
368+ <child>
369+ <object class="GtkLabel" id="max_download_speed_label">
370+ <property name="visible">True</property>
371+ <property name="xalign">1</property>
372+ <property name="xpad">5</property>
373+ <property name="label" translatable="yes">Max download speed (KiB/s)</property>
374+ </object>
375+ <packing>
376+ <property name="top_attach">2</property>
377+ <property name="bottom_attach">3</property>
378+ </packing>
379+ </child>
380+ <child>
381+ <object class="GtkSpinButton" id="max_upload_speed">
382+ <property name="visible">True</property>
383+ <property name="can_focus">True</property>
384+ <property name="invisible_char">•</property>
385+ <property name="activates_default">True</property>
386+ <property name="adjustment">adjustment1</property>
387+ <signal name="value-changed" handler="on_max_upload_speed_value_changed" swapped="no"/>
388+ </object>
389+ <packing>
390+ <property name="left_attach">1</property>
391+ <property name="right_attach">2</property>
392+ <property name="top_attach">1</property>
393+ <property name="bottom_attach">2</property>
394+ <property name="x_options">GTK_FILL</property>
395+ <property name="y_options">GTK_FILL</property>
396+ </packing>
397+ </child>
398+ <child>
399+ <object class="GtkSpinButton" id="max_download_speed">
400+ <property name="visible">True</property>
401+ <property name="can_focus">True</property>
402+ <property name="invisible_char">•</property>
403+ <property name="activates_default">True</property>
404+ <property name="adjustment">adjustment2</property>
405+ <signal name="value-changed" handler="on_max_download_speed_value_changed" swapped="no"/>
406+ </object>
407+ <packing>
408+ <property name="left_attach">1</property>
409+ <property name="right_attach">2</property>
410+ <property name="top_attach">2</property>
411+ <property name="bottom_attach">3</property>
412+ </packing>
413+ </child>
414+ <child>
415+ <placeholder/>
416+ </child>
417+ </object>
418+ <packing>
419+ <property name="expand">False</property>
420+ <property name="position">1</property>
421+ </packing>
422+ </child>
423+ </object>
424+ <packing>
425+ <property name="expand">False</property>
426+ <property name="position">0</property>
427+ </packing>
428+ </child>
429+ <child>
430+ <object class="GtkVButtonBox" id="vbuttonbox1">
431+ <property name="visible">True</property>
432+ <property name="layout_style">start</property>
433+ <child>
434+ <object class="GtkButton" id="remove">
435+ <property name="label">gtk-remove</property>
436+ <property name="visible">True</property>
437+ <property name="can_focus">True</property>
438+ <property name="receives_default">True</property>
439+ <property name="use_stock">True</property>
440+ <signal name="activate" handler="on_remove_clicked" swapped="no"/>
441+ <signal name="clicked" handler="on_remove_clicked" swapped="no"/>
442+ </object>
443+ <packing>
444+ <property name="expand">False</property>
445+ <property name="fill">False</property>
446+ <property name="position">0</property>
447+ </packing>
448+ </child>
449+ </object>
450+ <packing>
451+ <property name="expand">False</property>
452+ <property name="pack_type">end</property>
453+ <property name="position">1</property>
454+ </packing>
455+ </child>
456+ </object>
457+ <packing>
458+ <property name="expand">False</property>
459+ <property name="position">0</property>
460+ </packing>
461+ </child>
462+ <child>
463+ <object class="GtkLabel" id="warning_label">
464+ <property name="visible">True</property>
465+ </object>
466+ <packing>
467+ <property name="position">1</property>
468+ </packing>
469+ </child>
470+ </object>
471+</interface>
472
473=== modified file 'data/devices.ui'
474--- data/devices.ui 2010-12-06 12:27:11 +0000
475+++ data/devices.ui 2010-12-22 14:37:52 +0000
476@@ -7,7 +7,37 @@
477 <property name="border_width">10</property>
478 <property name="spacing">10</property>
479 <child>
480- <placeholder/>
481+ <object class="GtkScrolledWindow" id="scrolledwindow1">
482+ <property name="visible">True</property>
483+ <property name="can_focus">True</property>
484+ <property name="hscrollbar_policy">automatic</property>
485+ <property name="vscrollbar_policy">automatic</property>
486+ <child>
487+ <object class="GtkViewport" id="viewport1">
488+ <property name="visible">True</property>
489+ <property name="resize_mode">queue</property>
490+ <property name="shadow_type">none</property>
491+ <child>
492+ <object class="GtkAlignment" id="alignment1">
493+ <property name="visible">True</property>
494+ <property name="xscale">0</property>
495+ <property name="yscale">0</property>
496+ <child>
497+ <object class="GtkVBox" id="devices">
498+ <property name="visible">True</property>
499+ <child>
500+ <placeholder/>
501+ </child>
502+ </object>
503+ </child>
504+ </object>
505+ </child>
506+ </object>
507+ </child>
508+ </object>
509+ <packing>
510+ <property name="position">0</property>
511+ </packing>
512 </child>
513 </object>
514 </interface>
515
516=== modified file 'data/folders.ui'
517--- data/folders.ui 2010-12-06 12:27:11 +0000
518+++ data/folders.ui 2010-12-22 14:37:52 +0000
519@@ -7,7 +7,32 @@
520 <property name="border_width">10</property>
521 <property name="spacing">10</property>
522 <child>
523- <placeholder/>
524+ <object class="GtkScrolledWindow" id="scrolledwindow1">
525+ <property name="visible">True</property>
526+ <property name="can_focus">True</property>
527+ <property name="hscrollbar_policy">automatic</property>
528+ <property name="vscrollbar_policy">automatic</property>
529+ <child>
530+ <object class="GtkViewport" id="viewport1">
531+ <property name="visible">True</property>
532+ <property name="resize_mode">queue</property>
533+ <property name="shadow_type">none</property>
534+ <child>
535+ <object class="GtkAlignment" id="folders">
536+ <property name="visible">True</property>
537+ <property name="xscale">0</property>
538+ <property name="yscale">0</property>
539+ <child>
540+ <placeholder/>
541+ </child>
542+ </object>
543+ </child>
544+ </object>
545+ </child>
546+ </object>
547+ <packing>
548+ <property name="position">0</property>
549+ </packing>
550 </child>
551 </object>
552 </interface>
553
554=== modified file 'data/management.ui'
555--- data/management.ui 2010-12-06 12:27:11 +0000
556+++ data/management.ui 2010-12-22 14:37:52 +0000
557@@ -16,8 +16,23 @@
558 <property name="border_width">10</property>
559 <property name="spacing">10</property>
560 <child>
561- <object class="GtkProgressBar" id="quota_progressbar">
562+ <object class="GtkHBox" id="quota_box">
563 <property name="visible">True</property>
564+ <child>
565+ <object class="GtkAlignment" id="alignment1">
566+ <property name="visible">True</property>
567+ <property name="xscale">0</property>
568+ <property name="yscale">0</property>
569+ <child>
570+ <object class="GtkProgressBar" id="quota_progressbar">
571+ <property name="visible">True</property>
572+ </object>
573+ </child>
574+ </object>
575+ <packing>
576+ <property name="position">0</property>
577+ </packing>
578+ </child>
579 </object>
580 <packing>
581 <property name="expand">False</property>
582
583=== modified file 'debian/changelog'
584--- debian/changelog 2010-12-21 14:09:31 +0000
585+++ debian/changelog 2010-12-22 14:37:52 +0000
586@@ -1,3 +1,63 @@
587+ubuntuone-control-panel (0.1.0-0ubuntu1) UNRELEASED; urgency=low
588+
589+ * debian/control
590+ - depends on ubutuone-client >= 1.5.1
591+ - depends on ubuntu-sso-client >= 1.1.7
592+
593+ * New upstream release.
594+
595+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
596+
597+ * Updated list of messages to be shown on the overview panel (LP:
598+ #690379).
599+
600+ * Implemented callback for failure when loading devices.
601+
602+ * Added a spinner on every UbuntuOneBin to be shown while loading the
603+ panel content.
604+
605+ * Devices can now be removed (LP: #691295).
606+
607+ * Migrated dbus_client to use non-deprectaed SSO D-Bus API.
608+
609+ * Added clear_credentials to dbus_client module.
610+
611+ * Implemented devices tab (LP: #690649).
612+
613+ * Maximun size is set using geometry hints (LP: #683164).
614+
615+ * Added logging to dbus_service and backend.
616+
617+ * Error signals now sent the object id when available (LP: #691292).
618+
619+ * After machine was added, Folders page is shown (LP: #674459).*
620+ VolumesInfoError signal is now handled (LP: #690292).
621+
622+ * VolumesInfoError signal is now handled (LP: #690292).
623+
624+ * When FileSyncStatusError is received, no more DbusException messages
625+ are leaked to the end user (LP: #690305).
626+
627+ * User can now subcribe and unsubscribe from folders (LP: #689646).
628+
629+ * Dbus booleans are now those strings whose bool() defines its value. So,
630+ '' for False and any other non empty string for True (LP: #683619).
631+
632+ * File sync status is retrieved and displayed in the right top corner
633+ (LP: #673670).
634+
635+  * Adding handling for file sync disabled (only backend for now).
636+
637+  * Management panel is not twined itself if CredentialsFound signal is
638+ received several times (LP: #683649).
639+
640+ [ Rodney Dawes <rodney.dawes@canonical.com> ]
641+
642+ * Default to None and initialize if None in code, instead of mutable
643+ defaults.
644+
645+ -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Wed, 22 Dec 2010 11:15:04 -0300
646+
647 ubuntuone-control-panel (0.0.9-0ubuntu3) natty; urgency=low
648
649 * debian/control
650
651=== modified file 'debian/control'
652--- debian/control 2010-12-21 14:09:31 +0000
653+++ debian/control 2010-12-22 14:37:52 +0000
654@@ -35,8 +35,8 @@
655 python-simplejson,
656 python-twisted-core,
657 python-twisted-web,
658- python-ubuntuone-client (>= 1.5.0),
659- ubuntu-sso-client (>= 1.1.2),
660+ python-ubuntuone-client (>= 1.5.1),
661+ ubuntu-sso-client (>= 1.1.7),
662 Description: Ubuntu One Control Panel Python Libraries
663 Ubuntu One Control Panel provides a Python library to manage an Ubuntu One
664 account.
665@@ -50,8 +50,8 @@
666 python-dbus,
667 python-gobject,
668 python-gtk2,
669- python-ubuntuone-client (>= 1.5.0),
670- ubuntu-sso-client (>= 1.1.2),
671+ python-ubuntuone-client (>= 1.5.1),
672+ ubuntu-sso-client (>= 1.1.7),
673 ubuntuone-control-panel (= ${binary:Version}),
674 Description: Ubuntu One Control Panel
675 GTK+ desktop application to manage a Ubuntu One account.
676
677=== modified file 'po/POTFILES.in'
678--- po/POTFILES.in 2010-12-06 12:27:11 +0000
679+++ po/POTFILES.in 2010-12-22 14:37:52 +0000
680@@ -1,7 +1,6 @@
681 ubuntuone/controlpanel/gtk/gui.py
682 [type: gettext/glade] data/account.ui
683 [type: gettext/glade] data/applications.ui
684-[type: gettext/glade] data/controlpanel.ui
685 [type: gettext/glade] data/devices.ui
686 [type: gettext/glade] data/management.ui
687 [type: gettext/glade] data/overview.ui
688
689=== modified file 'setup.py'
690--- setup.py 2010-12-06 12:27:11 +0000
691+++ setup.py 2010-12-22 14:37:52 +0000
692@@ -75,7 +75,7 @@
693
694 DistUtilsExtra.auto.setup(
695 name='ubuntuone-control-panel',
696- version='0.0.9',
697+ version='0.1.0',
698 license='GPL v3',
699 author='Natalia Bidart',
700 author_email='natalia.bidart@canonical.com',
701
702=== modified file 'ubuntuone/controlpanel/__init__.py'
703--- ubuntuone/controlpanel/__init__.py 2010-12-06 12:27:11 +0000
704+++ ubuntuone/controlpanel/__init__.py 2010-12-22 14:37:52 +0000
705@@ -29,4 +29,4 @@
706 DBUS_PREFERENCES_PATH = "/preferences"
707 DBUS_PREFERENCES_IFACE = "com.ubuntuone.controlpanel.Preferences"
708
709-WEBSERVICE_BASE_URL = "https://one.ubuntu.com/api/"
710+WEBSERVICE_BASE_URL = u"https://one.ubuntu.com/api/"
711
712=== modified file 'ubuntuone/controlpanel/backend.py'
713--- ubuntuone/controlpanel/backend.py 2010-12-06 12:27:11 +0000
714+++ ubuntuone/controlpanel/backend.py 2010-12-22 14:37:52 +0000
715@@ -22,8 +22,12 @@
716 from twisted.internet.defer import inlineCallbacks, returnValue
717
718 from ubuntuone.controlpanel import dbus_client
719+from ubuntuone.controlpanel.logger import setup_logging, log_call
720 from ubuntuone.controlpanel.webclient import WebClient
721
722+
723+logger = setup_logging('backend')
724+
725 ACCOUNT_API = "account/"
726 QUOTA_API = "quota/"
727 DEVICES_API = "1.0/devices/"
728@@ -33,6 +37,22 @@
729 UPLOAD_KEY = "max_upload_speed"
730 DOWNLOAD_KEY = "max_download_speed"
731
732+FILE_SYNC_DISABLED = 'file-sync-disabled'
733+FILE_SYNC_DISCONNECTED = 'file-sync-disconnected'
734+FILE_SYNC_ERROR = 'file-sync-error'
735+FILE_SYNC_IDLE = 'file-sync-idle'
736+FILE_SYNC_STARTING = 'file-sync-starting'
737+FILE_SYNC_SYNCING = 'file-sync-syncing'
738+FILE_SYNC_UNKNOWN = 'file-sync-unknown'
739+
740+MSG_KEY = 'message'
741+STATUS_KEY = 'status'
742+
743+
744+def bool_str(value):
745+ """Return the string representation of a bool (dbus-compatible)."""
746+ return 'True' if value else ''
747+
748
749 class ControlBackend(object):
750 """The control panel backend."""
751@@ -40,20 +60,101 @@
752 def __init__(self):
753 """Initialize the webclient."""
754 self.wc = WebClient(dbus_client.get_credentials)
755+ self._status_changed_handler = None
756+ self.status_changed_handler = lambda *a: None
757+
758+ def _process_file_sync_status(self, status):
759+ """Process raw file sync status into custom format.
760+
761+ Return a dictionary with two members:
762+ * STATUS_KEY: the current status of syncdaemon, can be one of:
763+ FILE_SYNC_DISABLED, FILE_SYNC_STARTING, FILE_SYNC_DISCONNECTED,
764+ FILE_SYNC_SYNCING, FILE_SYNC_IDLE, FILE_SYNC_ERROR,
765+ FILE_SYNC_UNKNOWN
766+ * MSG_KEY: a non translatable but human readable string of the status.
767+
768+ """
769+ if not status:
770+ return {MSG_KEY: '', STATUS_KEY: FILE_SYNC_DISABLED}
771+
772+ msg = '%s (%s)' % (status['description'], status['name'])
773+ result = {MSG_KEY: msg}
774+
775+ # file synch is enabled
776+ is_error = bool(status['is_error'])
777+ is_synching = bool(status['is_connected'])
778+ is_idle = bool(status['is_online']) and status['queues'] == 'IDLE'
779+ is_disconnected = status['name'] == 'READY' and \
780+ 'Not User' in status['connection']
781+ is_starting = status['name'] in ('INIT', 'LOCAL_RESCAN', 'READY')
782+
783+ if is_error:
784+ result[STATUS_KEY] = FILE_SYNC_ERROR
785+ elif is_idle:
786+ result[STATUS_KEY] = FILE_SYNC_IDLE
787+ elif is_synching:
788+ result[STATUS_KEY] = FILE_SYNC_SYNCING
789+ elif is_disconnected:
790+ result[STATUS_KEY] = FILE_SYNC_DISCONNECTED
791+ elif is_starting:
792+ result[STATUS_KEY] = FILE_SYNC_STARTING
793+ else:
794+ logger.warning('file_sync_status: unknown (got %r)', status)
795+ result[STATUS_KEY] = FILE_SYNC_UNKNOWN
796+
797+ return result
798+
799+ def _set_status_changed_handler(self, handler):
800+ """Set 'handler' to be called when file sync status changes."""
801+ logger.debug('setting status_changed_handler to %r', handler)
802+
803+ def process_and_callback(status):
804+ """Process syncdaemon's status and callback 'handler'."""
805+ result = self._process_file_sync_status(status)
806+ handler(result)
807+
808+ self._status_changed_handler = process_and_callback
809+ dbus_client.set_status_changed_handler(process_and_callback)
810+
811+ def _get_status_changed_handler(self, handler):
812+ """Return the handler to be called when file sync status changes."""
813+ return self._status_changed_handler
814+
815+ status_changed_handler = property(_get_status_changed_handler,
816+ _set_status_changed_handler)
817
818 @inlineCallbacks
819 def get_token(self):
820- """Return the token from the credentials"""
821+ """Return the token from the credentials."""
822 credentials = yield dbus_client.get_credentials()
823 returnValue(credentials["token"])
824
825 @inlineCallbacks
826+ def device_is_local(self, device_id):
827+ """Return whether 'device_id' is the local devicew or not."""
828+ dtype, did = self.type_n_id(device_id)
829+ local_token = yield self.get_token()
830+ is_local = (dtype == DEVICE_TYPE_COMPUTER and did == local_token)
831+ logger.info('device_is_local: result %r, ', is_local)
832+ returnValue(is_local)
833+
834+ @log_call(logger.debug)
835+ @inlineCallbacks
836 def account_info(self):
837 """Get the user account info."""
838 result = {}
839
840 account_info = yield self.wc.call_api(ACCOUNT_API)
841- result["type"] = account_info["subscription"]["description"]
842+
843+ if "current_plan" in account_info:
844+ desc = account_info["current_plan"]
845+ elif "subscription" in account_info \
846+ and "description" in account_info["subscription"]:
847+ desc = account_info["subscription"]["description"]
848+ else:
849+ desc = ''
850+
851+ result["type"] = desc
852 result["name"] = account_info["nickname"]
853 result["email"] = account_info["email"]
854
855@@ -63,11 +164,11 @@
856
857 returnValue(result)
858
859+ @log_call(logger.debug)
860 @inlineCallbacks
861 def devices_info(self):
862 """Get the user devices info."""
863 result = []
864- this_token = yield self.get_token()
865 limit_bw = yield dbus_client.bandwidth_throttling_enabled()
866 limits = yield dbus_client.get_throttling_limits()
867
868@@ -77,19 +178,25 @@
869 result.append(di)
870 di["type"] = d["kind"]
871 di["name"] = d["description"]
872- di["configurable"] = "0"
873+ di["configurable"] = ''
874 if di["type"] == DEVICE_TYPE_COMPUTER:
875 di["device_id"] = di["type"] + d["token"]
876- if d["token"] == this_token:
877- di["configurable"] = "1"
878- di["limit_bandwidth"] = "1" if limit_bw else "0"
879- upload = limits["upload"]
880- download = limits["download"]
881- di[UPLOAD_KEY] = str(upload)
882- di[DOWNLOAD_KEY] = str(download)
883 if di["type"] == DEVICE_TYPE_PHONE:
884 di["device_id"] = di["type"] + str(d["id"])
885
886+ is_local = yield self.device_is_local(di["device_id"])
887+ di["is_local"] = bool_str(is_local)
888+ # currently, only local devices are configurable.
889+ # eventually, more the devices will be configurable.
890+ di["configurable"] = bool_str(is_local)
891+
892+ if bool(di["configurable"]):
893+ di["limit_bandwidth"] = bool_str(limit_bw)
894+ upload = limits["upload"]
895+ download = limits["download"]
896+ di[UPLOAD_KEY] = str(upload)
897+ di[DOWNLOAD_KEY] = str(download)
898+
899 # date_added is not in the webservice yet (LP: #673668)
900 # di["date_added"] = ""
901
902@@ -107,18 +214,17 @@
903 return DEVICE_TYPE_PHONE, device_id[5:]
904 return "No device", device_id
905
906+ @log_call(logger.info)
907 @inlineCallbacks
908 def change_device_settings(self, device_id, settings):
909 """Change the settings for the given device."""
910- dtype, did = self.type_n_id(device_id)
911- local_token = yield self.get_token()
912- is_local = (dtype == DEVICE_TYPE_COMPUTER and did == local_token)
913+ is_local = yield self.device_is_local(device_id)
914
915 if is_local and "limit_bandwidth" in settings:
916- limit_bw = settings["limit_bandwidth"]
917- if limit_bw == "0":
918+ limit_bw = bool(settings["limit_bandwidth"])
919+ if not limit_bw:
920 yield dbus_client.disable_bandwidth_throttling()
921- if limit_bw == "1":
922+ else:
923 yield dbus_client.enable_bandwidth_throttling()
924
925 if is_local and (UPLOAD_KEY in settings or
926@@ -137,30 +243,78 @@
927 # still pending: more work on the settings dict (LP: #673674)
928 returnValue(device_id)
929
930+ @log_call(logger.warning)
931 @inlineCallbacks
932 def remove_device(self, device_id):
933 """Remove a device's tokens from the sso server."""
934 dtype, did = self.type_n_id(device_id)
935- api = DEVICE_REMOVE_API % (dtype, did)
936+ is_local = yield self.device_is_local(device_id)
937+
938+ api = DEVICE_REMOVE_API % (dtype.lower(), did)
939 yield self.wc.call_api(api)
940+
941+ if is_local:
942+ logger.warning('remove_device: device is local, id %r, '
943+ 'clearing credentials.', device_id)
944+ yield dbus_client.clear_credentials()
945+
946 returnValue(device_id)
947
948+ @log_call(logger.debug)
949+ @inlineCallbacks
950 def file_sync_status(self):
951 """Return the status of the file sync service."""
952- # still pending (LP: #673670)
953- returnValue((True, "Synchronizing"))
954+ enabled = yield dbus_client.files_sync_enabled()
955+ if enabled:
956+ status = yield dbus_client.get_current_status()
957+ else:
958+ status = {}
959+ returnValue(self._process_file_sync_status(status))
960
961+ @log_call(logger.debug)
962 @inlineCallbacks
963 def volumes_info(self):
964- """Get the user defined volumes info."""
965- result = yield dbus_client.get_volumes()
966+ """Get the volumes info."""
967+ result = yield dbus_client.get_folders()
968+ # later, we may request shares info as well
969 returnValue(result)
970
971+ @log_call(logger.debug)
972+ @inlineCallbacks
973+ def change_volume_settings(self, volume_id, settings):
974+ """Change settings for 'volume_id'.
975+
976+ Currently, only supported setting is boolean 'subscribed'.
977+
978+ """
979+ if 'subscribed' in settings:
980+ subscribed = settings['subscribed']
981+ if subscribed:
982+ yield self.subscribe_volume(volume_id)
983+ else:
984+ yield self.unsubscribe_volume(volume_id)
985+
986+ @inlineCallbacks
987+ def subscribe_volume(self, volume_id):
988+ """Subscribe to 'volume_id'."""
989+ # when dealing with shares, we need to check if 'volume_id'
990+ # is a folder or a share
991+ yield dbus_client.subscribe_folder(volume_id)
992+
993+ @inlineCallbacks
994+ def unsubscribe_volume(self, volume_id):
995+ """Unsubscribe from 'volume_id'."""
996+ # when dealing with shares, we need to check if 'volume_id'
997+ # is a folder or a share
998+ yield dbus_client.unsubscribe_folder(volume_id)
999+
1000+ @log_call(logger.debug)
1001 def query_bookmark_extension(self):
1002 """True if the bookmark extension has been installed."""
1003 # still pending (LP: #673672)
1004 returnValue(False)
1005
1006+ @log_call(logger.debug)
1007 def install_bookmarks_extension(self):
1008 """Install the extension to sync bookmarks."""
1009 # still pending (LP: #673673)
1010
1011=== modified file 'ubuntuone/controlpanel/dbus_client.py'
1012--- ubuntuone/controlpanel/dbus_client.py 2010-12-06 12:27:11 +0000
1013+++ ubuntuone/controlpanel/dbus_client.py 2010-12-22 14:37:52 +0000
1014@@ -25,7 +25,8 @@
1015 from twisted.internet import defer
1016
1017 from ubuntuone.clientdefs import APP_NAME
1018-from ubuntuone.syncdaemon import dbus_interface as sd_dbus_iface
1019+from ubuntuone.platform.linux import dbus_interface as sd_dbus_iface
1020+from ubuntuone.platform.linux.tools import SyncDaemonTool
1021
1022 from ubuntuone.controlpanel.logger import setup_logging
1023
1024@@ -45,36 +46,88 @@
1025 """Do nothing"""
1026
1027
1028+def get_sso_proxy():
1029+ """Get a DBus proxy for credentials management."""
1030+ bus = dbus.SessionBus()
1031+ obj = bus.get_object(bus_name=ubuntu_sso.DBUS_BUS_NAME,
1032+ object_path=ubuntu_sso.DBUS_CREDENTIALS_PATH,
1033+ follow_name_owner_changes=True)
1034+ proxy = dbus.Interface(object=obj,
1035+ dbus_interface=ubuntu_sso.DBUS_CREDENTIALS_IFACE)
1036+ return proxy
1037+
1038+
1039 def get_credentials():
1040 """Get the credentials from the ubuntu-sso-client."""
1041 d = defer.Deferred()
1042- bus = dbus.SessionBus()
1043- obj = bus.get_object(bus_name=ubuntu_sso.DBUS_BUS_NAME,
1044- object_path=ubuntu_sso.DBUS_CRED_PATH,
1045- follow_name_owner_changes=True)
1046- proxy = dbus.Interface(object=obj,
1047- dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME)
1048+ proxy = get_sso_proxy()
1049
1050 def found_credentials(app_name, creds):
1051 """Credentials have been found."""
1052+ logger.debug('credentials were found for app_name %r.', app_name)
1053 if app_name == APP_NAME:
1054+ logger.info('credentials were found! (%r).', APP_NAME)
1055 d.callback(creds)
1056
1057- def credentials_error(app_name, error_message, detailed_error):
1058+ def credentials_error(app_name, error_dict=None):
1059 """No credentials could be retrieved."""
1060 if app_name == APP_NAME:
1061- error = CredentialsError(app_name, error_message, detailed_error)
1062+ if error_dict is None:
1063+ error_dict = {'error_message': 'Credentials were not found.',
1064+ 'detailed_error': ''}
1065+
1066+ logger.error('credentials error (%r, %r).', app_name, error_dict)
1067+ error = CredentialsError(app_name, error_dict)
1068 d.errback(error)
1069
1070 foundsig = proxy.connect_to_signal("CredentialsFound", found_credentials)
1071+ notfoundsig = proxy.connect_to_signal("CredentialsNotFound",
1072+ credentials_error)
1073 errorsig = proxy.connect_to_signal("CredentialsError", credentials_error)
1074- proxy.login_or_register_to_get_credentials(APP_NAME, "tcurl", "help", 0,
1075- reply_handler=no_op,
1076- error_handler=d.errback)
1077+ logger.debug('calling get_credentials.')
1078+ proxy.find_credentials(APP_NAME, {'': ''},
1079+ reply_handler=no_op, error_handler=d.errback)
1080
1081 def cleanup_signals(result):
1082 """Remove signals after use."""
1083 foundsig.remove()
1084+ notfoundsig.remove()
1085+ errorsig.remove()
1086+ return result
1087+
1088+ d.addBoth(cleanup_signals)
1089+ return d
1090+
1091+
1092+def clear_credentials():
1093+ """Clear the credentials using the ubuntu-sso-client."""
1094+ d = defer.Deferred()
1095+ proxy = get_sso_proxy()
1096+
1097+ def credentials_cleared(app_name):
1098+ """Credentials have been cleared."""
1099+ logger.debug('credentials were cleared for app_name %r.', app_name)
1100+ if app_name == APP_NAME:
1101+ logger.info('credentials were cleared! (%r).', APP_NAME)
1102+ d.callback(app_name)
1103+
1104+ def credentials_error(app_name, error_dict=None):
1105+ """No credentials could be retrieved."""
1106+ if app_name == APP_NAME:
1107+ logger.error('credentials error (%r, %r).', app_name, error_dict)
1108+ error = CredentialsError(app_name, error_dict)
1109+ d.errback(error)
1110+
1111+ clearedsig = proxy.connect_to_signal("CredentialsCleared",
1112+ credentials_cleared)
1113+ errorsig = proxy.connect_to_signal("CredentialsError", credentials_error)
1114+ logger.warning('calling clear_credentials.')
1115+ proxy.clear_credentials(APP_NAME, {'': ''},
1116+ reply_handler=no_op, error_handler=d.errback)
1117+
1118+ def cleanup_signals(result):
1119+ """Remove signals after use."""
1120+ clearedsig.remove()
1121 errorsig.remove()
1122 return result
1123
1124@@ -106,6 +159,12 @@
1125 sd_dbus_iface.DBUS_IFACE_FOLDERS_NAME)
1126
1127
1128+def get_status_syncdaemon_proxy():
1129+ """Get a DBus proxy for syncdaemon status calls."""
1130+ return get_syncdaemon_proxy("/status",
1131+ sd_dbus_iface.DBUS_IFACE_STATUS_NAME)
1132+
1133+
1134 def get_throttling_limits():
1135 """Get the speed limits from the syncdaemon."""
1136 d = defer.Deferred()
1137@@ -154,18 +213,16 @@
1138 return d
1139
1140
1141-def get_volumes():
1142- """Retrieve the volumes information from syncdaemon."""
1143- # grab folders
1144+def get_folders():
1145+ """Retrieve the folders information from syncdaemon."""
1146 d = defer.Deferred()
1147 proxy = get_folders_syncdaemon_proxy()
1148 proxy.get_folders(reply_handler=d.callback, error_handler=d.errback)
1149- # grab shares?
1150 return d
1151
1152
1153-def create_volume(path):
1154- """Create a new volume through syncdaemon."""
1155+def create_folder(path):
1156+ """Create a new folder through syncdaemon."""
1157 d = defer.Deferred()
1158 proxy = get_folders_syncdaemon_proxy()
1159
1160@@ -183,3 +240,72 @@
1161
1162 d.addBoth(cleanup_signals)
1163 return d
1164+
1165+
1166+def subscribe_folder(folder_id):
1167+ """Subscribe to 'folder_id'."""
1168+ d = defer.Deferred()
1169+ proxy = get_folders_syncdaemon_proxy()
1170+
1171+ sig = proxy.connect_to_signal('FolderSubscribed', d.callback)
1172+ cb = lambda folder_info, error: d.errback(VolumesError(folder_info, error))
1173+ esig = proxy.connect_to_signal('FolderSubscribeError', cb)
1174+
1175+ proxy.subscribe(folder_id, reply_handler=no_op, error_handler=no_op)
1176+
1177+ def cleanup_signals(result):
1178+ """Remove signals after use."""
1179+ sig.remove()
1180+ esig.remove()
1181+ return result
1182+
1183+ d.addBoth(cleanup_signals)
1184+ return d
1185+
1186+
1187+def unsubscribe_folder(folder_id):
1188+ """Unsubscribe 'folder_id'."""
1189+ d = defer.Deferred()
1190+ proxy = get_folders_syncdaemon_proxy()
1191+
1192+ sig = proxy.connect_to_signal('FolderUnSubscribed', d.callback)
1193+ cb = lambda folder_info, error: d.errback(VolumesError(folder_info, error))
1194+ esig = proxy.connect_to_signal('FolderUnSubscribeError', cb)
1195+
1196+ proxy.unsubscribe(folder_id, reply_handler=no_op, error_handler=no_op)
1197+
1198+ def cleanup_signals(result):
1199+ """Remove signals after use."""
1200+ sig.remove()
1201+ esig.remove()
1202+ return result
1203+
1204+ d.addBoth(cleanup_signals)
1205+ return d
1206+
1207+
1208+def get_current_status():
1209+ """Retrieve the current status from syncdaemon."""
1210+ d = defer.Deferred()
1211+ proxy = get_status_syncdaemon_proxy()
1212+ proxy.current_status(reply_handler=d.callback, error_handler=d.errback)
1213+ return d
1214+
1215+
1216+def set_status_changed_handler(handler):
1217+ """Connect 'handler' with syncdaemon's StatusChanged signal."""
1218+ proxy = get_status_syncdaemon_proxy()
1219+ sig = proxy.connect_to_signal('StatusChanged', handler)
1220+ return proxy, sig
1221+
1222+
1223+def files_sync_enabled():
1224+ """Get if file sync service is enabled."""
1225+ enabled = SyncDaemonTool(bus=None).is_files_sync_enabled()
1226+ return defer.succeed(enabled)
1227+
1228+
1229+@defer.inlineCallbacks
1230+def set_files_sync_enabled(enabled):
1231+ """Set the file sync service to be 'enabled'."""
1232+ yield SyncDaemonTool(bus=dbus.SessionBus()).enable_files_sync(enabled)
1233
1234=== modified file 'ubuntuone/controlpanel/dbus_service.py'
1235--- ubuntuone/controlpanel/dbus_service.py 2010-12-06 12:27:11 +0000
1236+++ ubuntuone/controlpanel/dbus_service.py 2010-12-22 14:37:52 +0000
1237@@ -28,9 +28,15 @@
1238 from twisted.python.failure import Failure
1239
1240 from ubuntuone.controlpanel import (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH,
1241- DBUS_PREFERENCES_IFACE, utils)
1242-from ubuntuone.controlpanel.backend import ControlBackend
1243-from ubuntuone.controlpanel.logger import setup_logging
1244+ DBUS_PREFERENCES_IFACE)
1245+from ubuntuone.controlpanel.backend import (
1246+ ControlBackend, FILE_SYNC_DISABLED, FILE_SYNC_DISCONNECTED,
1247+ FILE_SYNC_ERROR, FILE_SYNC_IDLE, FILE_SYNC_STARTING, FILE_SYNC_SYNCING,
1248+ MSG_KEY, STATUS_KEY,
1249+)
1250+from ubuntuone.controlpanel.logger import setup_logging, log_call
1251+from ubuntuone.controlpanel.utils import (ERROR_TYPE, ERROR_MESSAGE,
1252+ failure_to_error_dict, exception_to_error_dict)
1253
1254
1255 logger = setup_logging('dbus_service')
1256@@ -55,15 +61,15 @@
1257 """
1258 result = {}
1259 if isinstance(error, Failure):
1260- result = utils.failure_to_error_dict(error)
1261+ result = failure_to_error_dict(error)
1262 elif isinstance(error, Exception):
1263- result = utils.exception_to_error_dict(error)
1264+ result = exception_to_error_dict(error)
1265 elif isinstance(error, dict):
1266 # ensure that both keys and values are unicodes
1267 result = dict(map(make_unicode, i) for i in error.iteritems())
1268 else:
1269 msg = 'Got unexpected error argument %r' % error
1270- result = {utils.ERROR_TYPE: 'UnknownError', utils.ERROR_MESSAGE: msg}
1271+ result = {ERROR_TYPE: 'UnknownError', ERROR_MESSAGE: msg}
1272
1273 return result
1274
1275@@ -74,10 +80,14 @@
1276 With this call, a Failure is transformed into a string-string dict.
1277
1278 """
1279- def inner(error, *a):
1280+ def inner(error, _=None):
1281 """Do the Failure transformation."""
1282 error_dict = error_handler(error)
1283- return f(error_dict)
1284+ if _ is not None:
1285+ result = f(_, error_dict)
1286+ else:
1287+ result = f(error_dict)
1288+ return result
1289
1290 return inner
1291
1292@@ -89,10 +99,14 @@
1293 """Create this instance of the backend."""
1294 super(ControlPanelBackend, self).__init__(*args, **kwargs)
1295 self.backend = backend
1296- logger.debug('ControlPanelBackend created with %r, %r', args, kwargs)
1297+ self.backend.status_changed_handler = self.process_status
1298+ logger.debug('ControlPanelBackend: created with %r, %r.\n'
1299+ 'status_changed_handler is %r.',
1300+ args, kwargs, self.process_status)
1301
1302 # pylint: disable=C0103
1303
1304+ @log_call(logger.debug)
1305 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
1306 def account_info(self):
1307 """Find out the account info for the current logged in user."""
1308@@ -100,16 +114,19 @@
1309 d.addCallback(self.AccountInfoReady)
1310 d.addErrback(transform_failure(self.AccountInfoError))
1311
1312+ @log_call(logger.debug)
1313 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1314 def AccountInfoReady(self, info):
1315 """The info for the current user is available right now."""
1316
1317+ @log_call(logger.error)
1318 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1319 def AccountInfoError(self, error):
1320 """The info for the current user is currently unavailable."""
1321
1322 #---
1323
1324+ @log_call(logger.debug)
1325 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
1326 def devices_info(self):
1327 """Find out the devices info for the logged in user."""
1328@@ -117,67 +134,122 @@
1329 d.addCallback(self.DevicesInfoReady)
1330 d.addErrback(transform_failure(self.DevicesInfoError))
1331
1332+ @log_call(logger.debug)
1333 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")
1334 def DevicesInfoReady(self, info):
1335 """The info for the devices is available right now."""
1336
1337+ @log_call(logger.error)
1338 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1339 def DevicesInfoError(self, error):
1340 """The info for the devices is currently unavailable."""
1341
1342 #---
1343
1344+ @log_call(logger.info)
1345 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="sa{ss}")
1346- def change_device_settings(self, token, settings):
1347+ def change_device_settings(self, device_id, settings):
1348 """Configure a given device."""
1349- d = self.backend.change_device_settings(token, settings)
1350+ d = self.backend.change_device_settings(device_id, settings)
1351 d.addCallback(self.DeviceSettingsChanged)
1352- d.addErrback(transform_failure(self.DeviceSettingsChangeError))
1353+ d.addErrback(transform_failure(self.DeviceSettingsChangeError),
1354+ device_id)
1355
1356+ @log_call(logger.info)
1357 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
1358- def DeviceSettingsChanged(self, token):
1359+ def DeviceSettingsChanged(self, device_id):
1360 """The settings for the device were changed."""
1361
1362- @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1363- def DeviceSettingsChangeError(self, error):
1364+ @log_call(logger.error)
1365+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")
1366+ def DeviceSettingsChangeError(self, device_id, error):
1367 """Problem changing settings for the device."""
1368
1369 #---
1370
1371+ @log_call(logger.warning)
1372 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="s")
1373- def remove_device(self, token):
1374+ def remove_device(self, device_id):
1375 """Remove a given device."""
1376- d = self.backend.remove_device(token)
1377+ d = self.backend.remove_device(device_id)
1378 d.addCallback(self.DeviceRemoved)
1379- d.addErrback(transform_failure(self.DeviceRemovalError))
1380+ d.addErrback(transform_failure(self.DeviceRemovalError), device_id)
1381
1382+ @log_call(logger.warning)
1383 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
1384- def DeviceRemoved(self, token):
1385+ def DeviceRemoved(self, device_id):
1386 """The removal for the device was completed."""
1387
1388- @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1389- def DeviceRemovalError(self, error):
1390+ @log_call(logger.error)
1391+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")
1392+ def DeviceRemovalError(self, device_id, error):
1393 """Problem removing the device."""
1394
1395 #---
1396
1397+ def process_status(self, status_dict):
1398+ """Match status with signals."""
1399+ logger.info('process_status: new status received %r', status_dict)
1400+ status = status_dict[STATUS_KEY]
1401+ msg = status_dict[MSG_KEY]
1402+ if status == FILE_SYNC_DISABLED:
1403+ self.FileSyncStatusDisabled(msg)
1404+ elif status == FILE_SYNC_STARTING:
1405+ self.FileSyncStatusStarting(msg)
1406+ elif status == FILE_SYNC_DISCONNECTED:
1407+ self.FileSyncStatusDisconnected(msg)
1408+ elif status == FILE_SYNC_SYNCING:
1409+ self.FileSyncStatusSyncing(msg)
1410+ elif status == FILE_SYNC_IDLE:
1411+ self.FileSyncStatusIdle(msg)
1412+ elif status == FILE_SYNC_ERROR:
1413+ error_dict = {ERROR_TYPE: 'FileSyncStatusError',
1414+ ERROR_MESSAGE: msg}
1415+ self.FileSyncStatusError(error_dict)
1416+ else:
1417+ self.FileSyncStatusError(error_handler(status_dict))
1418+
1419+ @log_call(logger.debug)
1420 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
1421 def file_sync_status(self):
1422 """Get the status of the file sync service."""
1423 d = self.backend.file_sync_status()
1424- d.addCallback(lambda args: self.FileSyncStatusReady(*args))
1425+ d.addCallback(self.process_status)
1426 d.addErrback(transform_failure(self.FileSyncStatusError))
1427
1428- @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="bs")
1429- def FileSyncStatusReady(self, enabled, status):
1430- """The new file sync status is available."""
1431-
1432+ @log_call(logger.debug)
1433+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
1434+ def FileSyncStatusDisabled(self, msg):
1435+ """The file sync status is disabled."""
1436+
1437+ @log_call(logger.debug)
1438+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
1439+ def FileSyncStatusStarting(self, msg):
1440+ """The file sync service is starting."""
1441+
1442+ @log_call(logger.debug)
1443+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
1444+ def FileSyncStatusDisconnected(self, msg):
1445+ """The file sync service is waiting for user to request connection."""
1446+
1447+ @log_call(logger.debug)
1448+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
1449+ def FileSyncStatusSyncing(self, msg):
1450+ """The file sync service is currently syncing."""
1451+
1452+ @log_call(logger.debug)
1453+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
1454+ def FileSyncStatusIdle(self, msg):
1455+ """The file sync service is idle."""
1456+
1457+ @log_call(logger.error)
1458 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1459 def FileSyncStatusError(self, error):
1460 """Problem getting the file sync status."""
1461
1462 #---
1463
1464+ @log_call(logger.debug)
1465 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
1466 def volumes_info(self):
1467 """Find out the volumes info for the logged in user."""
1468@@ -185,35 +257,40 @@
1469 d.addCallback(self.VolumesInfoReady)
1470 d.addErrback(transform_failure(self.VolumesInfoError))
1471
1472- @utils.log_call(logger.info)
1473+ @log_call(logger.debug)
1474 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")
1475 def VolumesInfoReady(self, info):
1476 """The info for the volumes is available right now."""
1477
1478- @utils.log_call(logger.error)
1479+ @log_call(logger.error)
1480 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1481 def VolumesInfoError(self, error):
1482 """The info for the volumes is currently unavailable."""
1483
1484 #---
1485
1486+ @log_call(logger.info)
1487 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="sa{ss}")
1488 def change_volume_settings(self, volume_id, settings):
1489 """Configure a given volume."""
1490 d = self.backend.change_volume_settings(volume_id, settings)
1491 d.addCallback(self.VolumeSettingsChanged)
1492- d.addErrback(transform_failure(self.VolumeSettingsChangeError))
1493+ d.addErrback(transform_failure(self.VolumeSettingsChangeError),
1494+ volume_id)
1495
1496+ @log_call(logger.info)
1497 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
1498- def VolumeSettingsChanged(self, token):
1499+ def VolumeSettingsChanged(self, volume_id):
1500 """The settings for the volume were changed."""
1501
1502- @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1503- def VolumeSettingsChangeError(self, error):
1504+ @log_call(logger.error)
1505+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")
1506+ def VolumeSettingsChangeError(self, volume_id, error):
1507 """Problem changing settings for the volume."""
1508
1509 #---
1510
1511+ @log_call(logger.debug)
1512 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
1513 def query_bookmark_extension(self):
1514 """Check if the extension to sync bookmarks is installed."""
1515@@ -221,16 +298,19 @@
1516 d.addCallback(self.QueryBookmarksResult)
1517 d.addErrback(transform_failure(self.QueryBookmarksError))
1518
1519+ @log_call(logger.debug)
1520 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="b")
1521 def QueryBookmarksResult(self, enabled):
1522 """The bookmark extension is or is not installed."""
1523
1524+ @log_call(logger.error)
1525 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1526 def QueryBookmarksError(self, error):
1527 """Problem getting the status of the extension."""
1528
1529 #---
1530
1531+ @log_call(logger.info)
1532 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
1533 def install_bookmarks_extension(self):
1534 """Install the extension to sync bookmarks."""
1535@@ -238,10 +318,12 @@
1536 d.addCallback(lambda _: self.InstallBookmarksSuccess())
1537 d.addErrback(transform_failure(self.InstallBookmarksError))
1538
1539+ @log_call(logger.info)
1540 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="")
1541 def InstallBookmarksSuccess(self):
1542 """The extension to sync bookmarks has been installed."""
1543
1544+ @log_call(logger.error)
1545 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
1546 def InstallBookmarksError(self, error):
1547 """Problem installing the extension to sync bookmarks."""
1548@@ -274,8 +356,10 @@
1549 return dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
1550
1551
1552-def publish_backend(backend=ControlBackend()):
1553+def publish_backend(backend=None):
1554 """Publish the backend on the DBus."""
1555+ if backend is None:
1556+ backend = ControlBackend()
1557 return ControlPanelBackend(backend=backend,
1558 object_path=DBUS_PREFERENCES_PATH,
1559 bus_name=get_busname())
1560
1561=== modified file 'ubuntuone/controlpanel/gtk/gui.py'
1562--- ubuntuone/controlpanel/gtk/gui.py 2010-12-06 12:27:11 +0000
1563+++ ubuntuone/controlpanel/gtk/gui.py 2010-12-22 14:37:52 +0000
1564@@ -21,6 +21,7 @@
1565 from __future__ import division
1566
1567 import gettext
1568+import operator
1569
1570 from functools import wraps
1571
1572@@ -46,8 +47,10 @@
1573
1574 from ubuntuone.controlpanel import (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH,
1575 DBUS_PREFERENCES_IFACE)
1576-from ubuntuone.controlpanel.logger import setup_logging
1577-from ubuntuone.controlpanel.utils import get_data_file, log_call
1578+from ubuntuone.controlpanel.backend import (DEVICE_TYPE_PHONE,
1579+ DEVICE_TYPE_COMPUTER, bool_str)
1580+from ubuntuone.controlpanel.logger import setup_logging, log_call
1581+from ubuntuone.controlpanel.utils import get_data_file
1582
1583
1584 logger = setup_logging('gtk.gui')
1585@@ -58,16 +61,22 @@
1586 ORANGE = '#c95724'
1587 LOADING = _('Loading...')
1588 OVERVIEW_MSGS = [
1589- _('<b>Ubuntu One</b> Music Store. '
1590- 'A cloud enabled mobile shopping experience.\n'
1591- '<small>«this is custom text #1»</small>'),
1592- _('<i>Mobile</i> contacts sync. '
1593- '<span foreground="red">Your portable address book.</span>\n'
1594- '<small>«this is custom text #3»</small>'),
1595- _('...'),
1596- _('Sync data between computers.\n'
1597- '<small>«this is custom text #<i>N</i>»</small>')]
1598+ _('Backup and access your files from <b>any</b> computer (Ubuntu &amp; '
1599+ 'Windows), mobile device (Android) or the web.'),
1600+ _('A <b>portable address book</b> that unifies your most important '
1601+ 'contact sources: mobile phones, social networks, email clients and '
1602+ 'services.'),
1603+ _('A <b>personal music library</b> that can be streamed to Android, '
1604+ 'iPhone and AirPlay-enabled devices.'),
1605+ _('Add to that music library with a <b>Music Store</b> that automatically '
1606+ 'delivers to your personal cloud and connected devices.'),
1607+ _('Keep your <b>Firefox bookmarks</b> and <b>Tomboy notes</b> in sync '
1608+ 'across computers (Ubuntu &amp; Windows).'),
1609+ _('<b>2GB</b> of free storage.'),
1610+]
1611+VALUE_ERROR = _('Value could not be retrieved.')
1612 WARNING_MARKUP = '<span foreground="%s"><b>%%s</b></span>' % ORANGE
1613+KILOBYTES = 1024
1614
1615
1616 def filter_by_app_name(f):
1617@@ -85,16 +94,9 @@
1618 return filter_by_app_name_inner
1619
1620
1621-def dbus_str_to_bool(boolstr):
1622- """Transform a dbus string representation of a boolean to True/False."""
1623- return False if (not boolstr or boolstr == '0') else True
1624-
1625-
1626 class ControlPanelMixin(object):
1627 """The main interface for the Ubuntu One control panel."""
1628
1629- VALUE_ERROR = _('Value could not be retrieved.')
1630-
1631 def __init__(self, filename=None):
1632 bus = dbus.SessionBus()
1633 try:
1634@@ -155,6 +157,8 @@
1635 self.set_title(self.TITLE % {'app_name': U1_APP_NAME})
1636 self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
1637 self.set_icon_name('ubuntuone')
1638+ self.set_size_request(-1, 525) # bug #683164
1639+
1640 self.connect('delete-event', lambda w, e: gtk.main_quit())
1641 self.show()
1642
1643@@ -170,25 +174,49 @@
1644 gtk.main()
1645
1646
1647-class ControlPanel(gtk.VBox, ControlPanelMixin):
1648+class ControlPanel(gtk.Notebook):
1649 """The control panel per se, can be added into any other widget."""
1650
1651 # should not be any larger than 736x525
1652
1653 def __init__(self, window_id=0):
1654- gtk.VBox.__init__(self)
1655- ControlPanelMixin.__init__(self, filename='controlpanel.ui')
1656+ gtk.Notebook.__init__(self)
1657 self._window_id = window_id
1658
1659- self.overview = OverviewPanel(window_id=window_id)
1660+ self.set_show_tabs(False)
1661+ self.set_show_border(False)
1662+
1663+ self.overview = OverviewPanel(window_id=self._window_id)
1664+ self.insert_page(self.overview, position=0)
1665+
1666+ self.management = ManagementPanel()
1667+ self.insert_page(self.management, position=1)
1668+
1669 self.overview.connect('credentials-found',
1670 self.on_show_management_panel)
1671- self.add(self.overview)
1672+ self.management.connect('local-device-removed',
1673+ self.on_show_overview_panel)
1674+
1675 self.show()
1676-
1677- def on_show_management_panel(self, *args, **kwargs):
1678- """Show the netbook (main panel)."""
1679- self.add(ManagementPanel())
1680+ self.on_show_overview_panel()
1681+
1682+ logger.debug('%s: started (window size %r).',
1683+ self.__class__.__name__, self.get_size_request())
1684+
1685+ def on_show_overview_panel(self, widget=None):
1686+ """Show the overview panel."""
1687+ self.set_current_page(0)
1688+
1689+ def on_show_management_panel(self, widget=None,
1690+ credentials_are_new=False, token=None):
1691+ """Show the notebook (main panel)."""
1692+ if self.get_current_page() == 0:
1693+ self.management.load()
1694+ if credentials_are_new:
1695+ # redirect user to Folders page to review folders subscription
1696+ self.management.folders_button.clicked()
1697+
1698+ self.next_page()
1699
1700
1701 class UbuntuOneBin(gtk.VBox):
1702@@ -204,8 +232,26 @@
1703 self.title = PanelTitle(markup='<b>' + title + '</b>')
1704 self.pack_start(self.title, expand=False)
1705
1706+ self.message = LabelLoading(LOADING)
1707+ self.pack_start(self.message, expand=False)
1708+
1709 self.show_all()
1710
1711+ @log_call(logger.debug)
1712+ def on_success(self, message=''):
1713+ """Use this callback to stop the Loading and show 'message'."""
1714+ self.message.stop()
1715+ self.message.set_markup(message)
1716+
1717+ @log_call(logger.error)
1718+ def on_error(self, message=None):
1719+ """Use this callback to stop the Loading and set a warning message."""
1720+ if message == None:
1721+ message = VALUE_ERROR
1722+
1723+ self.message.stop()
1724+ self.message.set_markup(WARNING_MARKUP % message)
1725+
1726
1727 class OverviewPanel(GreyableBin, ControlPanelMixin):
1728 """The overview panel. Introduces Ubuntu One to the not logged user."""
1729@@ -222,7 +268,6 @@
1730 BULLET = '<span foreground="%s">✔</span>' % ORANGE
1731
1732 def __init__(self, messages=None, window_id=0):
1733- gtk.link_button_set_uri_hook(lambda *a: None)
1734 GreyableBin.__init__(self)
1735 ControlPanelMixin.__init__(self, filename='overview.ui')
1736 self.add(self.itself)
1737@@ -230,6 +275,9 @@
1738 self.warning_label.set_property('xalign', 0.5)
1739 self.warning_label.connect('size-allocate', self.on_size_allocate)
1740
1741+ self.connect_button.set_uri('')
1742+
1743+ self._credentials_are_new = False
1744 self._messages = messages
1745 self._window_id = window_id
1746 self._build_messages()
1747@@ -320,13 +368,14 @@
1748 @log_call(logger.info)
1749 def on_credentials_found(self, app_name, credentials):
1750 """SSO backend notifies of credentials found."""
1751- self.emit('credentials-found', app_name, credentials)
1752- self.hide()
1753+ self.set_property('greyed', False)
1754+ self.emit('credentials-found', self._credentials_are_new, credentials)
1755
1756 @filter_by_app_name
1757 @log_call(logger.info)
1758 def on_credentials_not_found(self, app_name):
1759 """SSO backend notifies of credentials not found."""
1760+ self._credentials_are_new = True
1761 self.set_property('greyed', False)
1762
1763 @filter_by_app_name
1764@@ -365,42 +414,36 @@
1765 """The account panel. The user can manage the subscription."""
1766
1767 TITLE = _('Welcome to Ubuntu One!')
1768+ NAME = _('Name')
1769+ TYPE = _('Account type')
1770+ EMAIL = _('Email address')
1771
1772 def __init__(self):
1773 UbuntuOneBin.__init__(self)
1774 ControlPanelMixin.__init__(self, filename='account.ui')
1775- self.pack_start(self.itself)
1776+ self.add(self.itself)
1777 self.show()
1778
1779 self.backend.connect_to_signal('AccountInfoReady',
1780 self.on_account_info_ready)
1781 self.backend.connect_to_signal('AccountInfoError',
1782 self.on_account_info_error)
1783-
1784- self.name_label = LabelLoading(LOADING)
1785- self.name_box.pack_start(self.name_label)
1786-
1787- self.type_label = LabelLoading(LOADING)
1788- self.type_box.pack_start(self.type_label)
1789-
1790- self.email_label = LabelLoading(LOADING)
1791- self.email_box.pack_start(self.email_label)
1792+ self.account.hide()
1793
1794 @log_call(logger.debug)
1795 def on_account_info_ready(self, info):
1796 """Backend notifies of account info."""
1797- for i in ('name', 'type', 'email'):
1798+ self.on_success()
1799+
1800+ for i in (u'name', u'type', u'email'):
1801 label = getattr(self, '%s_label' % i)
1802- label.set_markup('<b>%s</b>' % info[i])
1803- label.stop()
1804+ label.set_markup('%s' % (info[i]))
1805+ self.account.show()
1806
1807 @log_call(logger.error)
1808 def on_account_info_error(self, error_dict=None):
1809 """Backend notifies of an error when fetching account info."""
1810- for i in ('name', 'type', 'email'):
1811- label = getattr(self, '%s_label' % i)
1812- label.set_markup(WARNING_MARKUP % self.VALUE_ERROR)
1813- label.stop()
1814+ self.on_error()
1815
1816
1817 class FoldersPanel(UbuntuOneBin, ControlPanelMixin):
1818@@ -408,48 +451,241 @@
1819
1820 TITLE = _('Listed below are the folders available on this machine. '
1821 'Subscribed means the folder will receive and send updates.')
1822+ NO_VOLUMES = _('No folders to show.')
1823
1824 def __init__(self):
1825 UbuntuOneBin.__init__(self)
1826 ControlPanelMixin.__init__(self, filename='folders.ui')
1827- self.pack_start(self.itself)
1828+ self.add(self.itself)
1829 self.show_all()
1830
1831- self.header = (gtk.Label(), gtk.Label())
1832- self.header[0].set_markup('<b>' + _('Local path') + '</b>')
1833- self.header[1].set_markup('<b>' + _('Subscribed') + '</b>')
1834-
1835 self.backend.connect_to_signal('VolumesInfoReady',
1836 self.on_volumes_info_ready)
1837 self.backend.connect_to_signal('VolumesInfoError',
1838 self.on_volumes_info_error)
1839- self.backend.volumes_info()
1840+ self.volumes = None
1841+ self._subscribed = []
1842
1843- @log_call(logger.debug)
1844 def on_volumes_info_ready(self, info):
1845 """Backend notifies of volumes info."""
1846- folders = gtk.Table(rows=len(info) + 1, columns=2)
1847-
1848- folders.attach(self.header[0], 0, 1, 0, 1)
1849- folders.attach(self.header[1], 1, 2, 0, 1, xoptions=0)
1850-
1851+
1852+ if self.volumes is not None:
1853+ self.folders.remove(self.volumes)
1854+ self.volumes = None
1855+
1856+ if not info:
1857+ self.on_success(self.NO_VOLUMES)
1858+ return
1859+ else:
1860+ self.on_success()
1861+
1862+ self.volumes = gtk.Table(rows=len(info) + 1, columns=2)
1863+
1864+ header = (gtk.Label(), gtk.Label())
1865+ header[0].set_markup('<b>' + _('Local path') + '</b>')
1866+ header[1].set_markup('<b>' + _('Subscribed') + '</b>')
1867+ self.volumes.attach(header[0], 0, 1, 0, 1)
1868+ self.volumes.attach(header[1], 1, 2, 0, 1, xoptions=0)
1869+ self.volumes.show_all()
1870+
1871+ info.sort(key=operator.itemgetter('suggested_path'))
1872 for i, volume in enumerate(info):
1873- path = gtk.Label(volume['path'])
1874+ path = gtk.Label(volume['suggested_path'])
1875 path.set_property('xalign', 0)
1876- folders.attach(path, 0, 1, i + 1, i + 2)
1877-
1878- subscribed = gtk.CheckButton()
1879- subscribed.set_active(dbus_str_to_bool(volume['subscribed']))
1880- folders.attach(subscribed, 1, 2, i + 1, i + 2, xoptions=0)
1881-
1882- alig = gtk.Alignment(xalign=0.5)
1883- alig.add(folders)
1884- alig.show_all()
1885- self.itself.pack_start(alig, expand=False)
1886+ path.show()
1887+ self.volumes.attach(path, 0, 1, i + 1, i + 2)
1888+
1889+ subscribed = gtk.CheckButton(volume['volume_id'])
1890+ subscribed.set_active(bool(volume['subscribed']))
1891+ subscribed.show()
1892+ subscribed.get_child().hide()
1893+ subscribed.connect('clicked', self.on_subscribed_clicked)
1894+ self._subscribed.append(subscribed)
1895+ self.volumes.attach(subscribed, 1, 2, i + 1, i + 2, xoptions=0)
1896+
1897+ self.folders.add(self.volumes)
1898
1899 @log_call(logger.error)
1900 def on_volumes_info_error(self, error_dict=None):
1901 """Backend notifies of an error when fetching volumes info."""
1902+ self.on_error()
1903+
1904+ def on_subscribed_clicked(self, checkbutton):
1905+ """The user toggled 'checkbutton'."""
1906+ volume_id = checkbutton.get_label()
1907+ subscribed = bool_str(checkbutton.get_active())
1908+ self.backend.change_volume_settings(volume_id,
1909+ {'subscribed': subscribed})
1910+
1911+ def load(self):
1912+ """Load the volume list."""
1913+ self.backend.volumes_info()
1914+ self.message.start()
1915+
1916+
1917+class Device(gtk.VBox, ControlPanelMixin):
1918+ """The device widget."""
1919+
1920+ DEVICE_CHANGE_ERROR = _('The settings could not be changed,\n'
1921+ 'previous values were restored.')
1922+ DEVICE_REMOVAL_ERROR = _('The device could not be removed.')
1923+
1924+ def __init__(self):
1925+ gtk.VBox.__init__(self)
1926+ ControlPanelMixin.__init__(self, filename='device.ui')
1927+
1928+ self._updating = False
1929+ self._last_settings = {}
1930+ self.id = None
1931+ self.is_local = False
1932+ self.configurable = False
1933+
1934+ self.update(device_id=None, device_name='',
1935+ is_local=False, configurable=False, limit_bandwidth=False,
1936+ max_upload_speed=0, max_download_speed=0)
1937+
1938+ self.add(self.itself)
1939+ self.show()
1940+
1941+ self.backend.connect_to_signal('DeviceSettingsChanged',
1942+ self.on_device_settings_changed)
1943+ self.backend.connect_to_signal('DeviceSettingsChangeError',
1944+ self.on_device_settings_change_error)
1945+ self.backend.connect_to_signal('DeviceRemoved',
1946+ self.on_device_removed)
1947+ self.backend.connect_to_signal('DeviceRemovalError',
1948+ self.on_device_removal_error)
1949+
1950+ def _change_device_settings(self, *args):
1951+ """Update backend settings for this device."""
1952+ if self._updating:
1953+ return
1954+
1955+ # Not disabling the GUI to avoid annyong twitchings
1956+ #self.set_sensitive(False)
1957+ self.warning_label.set_text('')
1958+ self.backend.change_device_settings(self.id, self.__dict__)
1959+
1960+ def _block_signals(f):
1961+ """Execute 'f' while having the _updating flag set."""
1962+
1963+ # pylint: disable=E0213,W0212,E1102
1964+
1965+ @wraps(f)
1966+ def inner(self, *args, **kwargs):
1967+ """Execute 'f' while having the _updating flag set."""
1968+ old = self._updating
1969+ self._updating = True
1970+
1971+ result = f(self, *args, **kwargs)
1972+
1973+ self._updating = old
1974+ return result
1975+
1976+ return inner
1977+
1978+ on_limit_bandwidth_toggled = _change_device_settings
1979+ on_max_upload_speed_value_changed = _change_device_settings
1980+ on_max_download_speed_value_changed = _change_device_settings
1981+
1982+ def on_remove_clicked(self, widget):
1983+ """Remove button was clicked or activated."""
1984+ self.backend.remove_device(self.id)
1985+ self.set_sensitive(False)
1986+
1987+ @_block_signals
1988+ def update(self, **kwargs):
1989+ """Update according to named parameters.
1990+
1991+ Possible settings are:
1992+ * device_id (string, not shown to the user)
1993+ * device_name (string)
1994+ * type (either DEVICE_TYPE_PHONE or DEVICE_TYPE_COMPUTER)
1995+ * is_local (True/False)
1996+ * configurable (True/False)
1997+ * if configurable, the following can be set:
1998+ * limit_bandwidth (True/False)
1999+ * max_upload_speed (bytes)
2000+ * max_download_speed (bytes)
2001+
2002+ """
2003+ if 'device_id' in kwargs:
2004+ self.id = kwargs['device_id']
2005+
2006+ if 'device_name' in kwargs:
2007+ self.device_name.set_markup('<b>%s</b>' % kwargs['device_name'])
2008+
2009+ if 'device_type' in kwargs:
2010+ dtype = kwargs['device_type']
2011+ if dtype in (DEVICE_TYPE_COMPUTER, DEVICE_TYPE_PHONE):
2012+ self.device_type.set_from_icon_name(dtype.lower(),
2013+ gtk.ICON_SIZE_BUTTON)
2014+
2015+ if 'is_local' in kwargs:
2016+ self.is_local = bool(kwargs['is_local'])
2017+
2018+ if 'configurable' in kwargs:
2019+ self.configurable = bool(kwargs['configurable'])
2020+ self.throttling.set_visible(self.configurable)
2021+
2022+ if 'limit_bandwidth' in kwargs:
2023+ self.limit_bandwidth.set_active(bool(kwargs['limit_bandwidth']))
2024+
2025+ for speed in ('max_upload_speed', 'max_download_speed'):
2026+ if speed in kwargs:
2027+ value = int(kwargs[speed]) // KILOBYTES
2028+ getattr(self, speed).set_value(value)
2029+
2030+ self._last_settings = self.__dict__
2031+
2032+ @property
2033+ def __dict__(self):
2034+ result = {
2035+ 'device_id': self.id,
2036+ 'device_name': self.device_name.get_text(),
2037+ 'device_type': self.device_type.get_icon_name()[0].capitalize(),
2038+ 'is_local': bool_str(self.is_local),
2039+ 'configurable': bool_str(self.configurable),
2040+ 'limit_bandwidth': bool_str(self.limit_bandwidth.get_active()),
2041+ 'max_upload_speed': \
2042+ str(self.max_upload_speed.get_value_as_int() * KILOBYTES),
2043+ 'max_download_speed': \
2044+ str(self.max_download_speed.get_value_as_int() * KILOBYTES),
2045+ }
2046+ return result
2047+
2048+ @log_call(logger.info)
2049+ def on_device_settings_changed(self, device_id):
2050+ """The change of this device settings succeded."""
2051+ if device_id != self.id:
2052+ return
2053+ self.set_sensitive(True)
2054+ self.warning_label.set_text('')
2055+ self._last_settings = self.__dict__
2056+
2057+ @log_call(logger.error)
2058+ def on_device_settings_change_error(self, device_id, error_dict=None):
2059+ """The change of this device settings failed."""
2060+ if device_id != self.id:
2061+ return
2062+ self.update(**self._last_settings)
2063+ self._set_warning(self.DEVICE_CHANGE_ERROR, self.warning_label)
2064+ self.set_sensitive(True)
2065+
2066+ @log_call(logger.warning)
2067+ def on_device_removed(self, device_id):
2068+ """The removal of this device succeded."""
2069+ if device_id != self.id:
2070+ return
2071+ self.hide()
2072+
2073+ @log_call(logger.error)
2074+ def on_device_removal_error(self, device_id, error_dict=None):
2075+ """The removal of this device failed."""
2076+ if device_id != self.id:
2077+ return
2078+ self._set_warning(self.DEVICE_REMOVAL_ERROR, self.warning_label)
2079+ self.set_sensitive(True)
2080
2081
2082 class DevicesPanel(UbuntuOneBin, ControlPanelMixin):
2083@@ -457,13 +693,62 @@
2084
2085 TITLE = _('The devices connected with your personal cloud network are '
2086 'listed below:')
2087+ NO_DEVICES = _('No devices to show.')
2088
2089 def __init__(self):
2090 UbuntuOneBin.__init__(self)
2091 ControlPanelMixin.__init__(self, filename='devices.ui')
2092- self.pack_start(self.itself)
2093+ self.add(self.itself)
2094 self.show()
2095
2096+ self._devices = {}
2097+
2098+ self.backend.connect_to_signal('DevicesInfoReady',
2099+ self.on_devices_info_ready)
2100+ self.backend.connect_to_signal('DevicesInfoError',
2101+ self.on_devices_info_error)
2102+ self.backend.connect_to_signal('DeviceRemoved',
2103+ self.on_device_removed)
2104+
2105+ def on_devices_info_ready(self, info):
2106+ """Backend notifies of devices info."""
2107+ for child in self.devices.get_children():
2108+ self.devices.remove(child)
2109+
2110+ if not info:
2111+ self.on_success(self.NO_DEVICES)
2112+ else:
2113+ self.on_success()
2114+
2115+ for device_info in info:
2116+ device = Device()
2117+ device_info['device_name'] = device_info.pop('name', '')
2118+ device_info['device_type'] = device_info.pop('type',
2119+ DEVICE_TYPE_COMPUTER)
2120+ device.update(**device_info)
2121+ self.devices.pack_start(device)
2122+ self._devices[device.id] = device
2123+
2124+ @log_call(logger.error)
2125+ def on_devices_info_error(self, error_dict=None):
2126+ """Backend notifies of an error when fetching volumes info."""
2127+ self.on_error()
2128+
2129+ @log_call(logger.warning)
2130+ def on_device_removed(self, device_id):
2131+ """The removal of a device succeded."""
2132+ if device_id in self._devices:
2133+ child = self._devices.pop(device_id)
2134+ self.devices.remove(child)
2135+
2136+ if child.is_local:
2137+ self.emit('local-device-removed')
2138+
2139+ def load(self):
2140+ """Load the device list."""
2141+ self.backend.devices_info()
2142+ self.message.start()
2143+
2144
2145 class ApplicationsPanel(UbuntuOneBin, ControlPanelMixin):
2146 """The applications panel."""
2147@@ -474,9 +759,12 @@
2148 def __init__(self):
2149 UbuntuOneBin.__init__(self)
2150 ControlPanelMixin.__init__(self, filename='applications.ui')
2151- self.pack_start(self.itself)
2152+ self.add(self.itself)
2153 self.show()
2154
2155+ self.message.stop()
2156+ self.message.set_text('Under construction')
2157+
2158
2159 class ManagementPanel(gtk.VBox, ControlPanelMixin):
2160 """The management panel.
2161@@ -486,11 +774,21 @@
2162 """
2163
2164 QUOTA_LABEL = _('%(used)s used of %(total)s (%(percentage).1f%%)')
2165+ FILE_SYNC_DISABLED = _('File synchronization service is not enabled.')
2166+ FILE_SYNC_STARTING = _('File synchronization service is starting,\n'
2167+ 'please wait...')
2168+ FILE_SYNC_DISCONNECTED = _('File synchronization service is ready,\nplease'
2169+ ' connect it to access your personal cloud. ')
2170+ FILE_SYNC_SYNCING = _('File synchronization service is fully functional,\n'
2171+ 'performing synchronization now...')
2172+ FILE_SYNC_IDLE = _('File synchronization service is idle,\n'
2173+ 'all the files are synchronized.')
2174+ FILE_SYNC_ERROR = _('File synchronization status can not be retrieved.')
2175
2176 def __init__(self):
2177 gtk.VBox.__init__(self)
2178 ControlPanelMixin.__init__(self, filename='management.ui')
2179- self.pack_start(self.itself)
2180+ self.add(self.itself)
2181 self.show()
2182
2183 self.backend.connect_to_signal('AccountInfoReady',
2184@@ -498,10 +796,25 @@
2185 self.backend.connect_to_signal('AccountInfoError',
2186 self.on_account_info_error)
2187
2188+ self.backend.connect_to_signal('FileSyncStatusDisabled',
2189+ self.on_file_sync_status_disabled)
2190+ self.backend.connect_to_signal('FileSyncStatusStarting',
2191+ self.on_file_sync_status_starting)
2192+ self.backend.connect_to_signal('FileSyncStatusDisconnected',
2193+ self.on_file_sync_status_disconnected)
2194+ self.backend.connect_to_signal('FileSyncStatusSyncing',
2195+ self.on_file_sync_status_syncing)
2196+ self.backend.connect_to_signal('FileSyncStatusIdle',
2197+ self.on_file_sync_status_idle)
2198+ self.backend.connect_to_signal('FileSyncStatusError',
2199+ self.on_file_sync_status_error)
2200+
2201 self.header.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(DEFAULT_BG))
2202+
2203 self.quota_label = LabelLoading(LOADING, fg_color=DEFAULT_FG)
2204+ self.quota_box.pack_start(self.quota_label, expand=False)
2205+
2206 self.status_label = LabelLoading(LOADING, fg_color=DEFAULT_FG)
2207- self.status_box.pack_start(self.quota_label, expand=False)
2208 self.status_box.pack_end(self.status_label, expand=False)
2209
2210 self.account = AccountPanel()
2211@@ -520,7 +833,10 @@
2212 gtk.gdk.Color(DEFAULT_FG))
2213 self.notebook.insert_page(getattr(self, tab), position=page_num)
2214
2215- self.backend.account_info()
2216+ self.folders_button.connect('clicked', lambda b: self.folders.load())
2217+ self.devices_button.connect('clicked', lambda b: self.devices.load())
2218+ self.devices.connect('local-device-removed',
2219+ lambda widget: self.emit('local-device-removed'))
2220
2221 def _update_quota(self, msg, data=None):
2222 """Update the quota info."""
2223@@ -532,6 +848,17 @@
2224 fraction = data.get('percentage', 0.0) / 100
2225 self.quota_progressbar.set_fraction(fraction)
2226
2227+ def _update_status(self, msg):
2228+ """Update the status info."""
2229+ self.status_label.set_markup(msg)
2230+ self.status_label.stop()
2231+
2232+ def load(self):
2233+ """Load the account info and file sync status list."""
2234+ self.backend.account_info()
2235+ self.backend.file_sync_status()
2236+ self.account_button.clicked()
2237+
2238 @log_call(logger.debug)
2239 def on_account_info_ready(self, info):
2240 """Backend notifies of account info."""
2241@@ -544,9 +871,42 @@
2242 @log_call(logger.error)
2243 def on_account_info_error(self, error_dict=None):
2244 """Backend notifies of an error when fetching account info."""
2245- self._update_quota(WARNING_MARKUP % self.VALUE_ERROR)
2246+ self._update_quota(WARNING_MARKUP % VALUE_ERROR)
2247+
2248+ @log_call(logger.info)
2249+ def on_file_sync_status_disabled(self, msg):
2250+ """Backend notifies of file sync status being disabled."""
2251+ self._update_status(self.FILE_SYNC_DISABLED)
2252+
2253+ @log_call(logger.info)
2254+ def on_file_sync_status_starting(self, msg):
2255+ """Backend notifies of file sync status being starting."""
2256+ self._update_status(self.FILE_SYNC_STARTING)
2257+
2258+ @log_call(logger.info)
2259+ def on_file_sync_status_disconnected(self, msg):
2260+ """Backend notifies of file sync status being ready."""
2261+ self._update_status(self.FILE_SYNC_DISCONNECTED)
2262+
2263+ @log_call(logger.info)
2264+ def on_file_sync_status_syncing(self, msg):
2265+ """Backend notifies of file sync status being syncing."""
2266+ self._update_status(self.FILE_SYNC_SYNCING)
2267+
2268+ @log_call(logger.info)
2269+ def on_file_sync_status_idle(self, msg):
2270+ """Backend notifies of file sync status being idle."""
2271+ self._update_status(self.FILE_SYNC_IDLE)
2272+
2273+ @log_call(logger.error)
2274+ def on_file_sync_status_error(self, error_dict=None):
2275+ """Backend notifies of an error when fetching file sync status."""
2276+ self._update_status(WARNING_MARKUP % self.FILE_SYNC_ERROR)
2277
2278
2279 gobject.signal_new('credentials-found', OverviewPanel,
2280 gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
2281- (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT))
2282+ (gobject.TYPE_BOOLEAN, gobject.TYPE_PYOBJECT))
2283+
2284+gobject.signal_new('local-device-removed', DevicesPanel,
2285+ gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
2286
2287=== modified file 'ubuntuone/controlpanel/gtk/tests/test_gui.py'
2288--- ubuntuone/controlpanel/gtk/tests/test_gui.py 2010-12-06 12:27:11 +0000
2289+++ ubuntuone/controlpanel/gtk/tests/test_gui.py 2010-12-22 14:37:52 +0000
2290@@ -40,9 +40,28 @@
2291 'email': 'test.com', 'quota_total': '12345', 'quota_used': '9999'}
2292
2293 FAKE_VOLUMES_INFO = [
2294- {'volume_id': '0', 'path': '~/foo', 'subscribed': '0'},
2295- {'volume_id': '1', 'path': '~/bar', 'subscribed': '1'},
2296- {'volume_id': '2', 'path': '~/baz', 'subscribed': '1'},
2297+ {'volume_id': '0', 'suggested_path': '~/foo', 'subscribed': ''},
2298+ {'volume_id': '1', 'suggested_path': '~/bar', 'subscribed': 'True'},
2299+ {'volume_id': '2', 'suggested_path': '~/baz', 'subscribed': 'True'},
2300+]
2301+
2302+FAKE_DEVICE_INFO = {
2303+ 'device_id': '1258-6854', 'device_name': 'Baz', 'device_type': 'Computer',
2304+ 'is_local': 'True', 'configurable': 'True', 'limit_bandwidth': 'True',
2305+ 'max_upload_speed': '1000', 'max_download_speed': '72548',
2306+}
2307+
2308+FAKE_DEVICES_INFO = [
2309+ {'device_id': '0', 'name': 'Foo', 'type': 'Computer',
2310+ 'is_local': '', 'configurable': ''},
2311+ {'device_id': '1', 'name': 'Bar', 'type': 'Phone',
2312+ 'is_local': '', 'configurable': ''},
2313+ {'device_id': '2', 'name': 'Z', 'type': 'Computer',
2314+ 'is_local': '', 'configurable': 'True', 'limit_bandwidth': '',
2315+ 'max_upload_speed': '0', 'max_download_speed': '0'},
2316+ {'device_id': '1258-6854', 'name': 'Baz', 'type': 'Computer',
2317+ 'is_local': 'True', 'configurable': 'True', 'limit_bandwidth': 'True',
2318+ 'max_upload_speed': '1000', 'max_download_speed': '72548'}, # local
2319 ]
2320
2321
2322@@ -108,8 +127,11 @@
2323 bus_name = gui.DBUS_BUS_NAME
2324 object_path = gui.DBUS_PREFERENCES_PATH
2325 iface = gui.DBUS_PREFERENCES_IFACE
2326- exposed_methods = ['account_info', 'devices_info', 'volumes_info',
2327- 'file_sync_status']
2328+ exposed_methods = [
2329+ 'account_info', 'devices_info', 'change_device_settings',
2330+ 'volumes_info', 'change_volume_settings', 'file_sync_status',
2331+ 'remove_device',
2332+ ]
2333
2334
2335 class FakedSessionBus(object):
2336@@ -193,7 +215,6 @@
2337 def assert_warning_correct(self, warning, text):
2338 """Check that 'warning' is visible, showing 'text'."""
2339 self.assertTrue(warning.get_visible(), 'Must be visible.')
2340- self.assertEqual(warning.get_text(), text)
2341 self.assertEqual(warning.get_label(), gui.WARNING_MARKUP % text)
2342
2343 def assert_function_decorated(self, decorator, func):
2344@@ -268,29 +289,39 @@
2345 self.assertEqual(self.ui.get_icon_name(), 'ubuntuone')
2346
2347 def test_max_size(self):
2348- """Max size is not bigger than 966x576 (LP: #645526)."""
2349- self.assertTrue(self.ui.get_size_request() <= (966, 576))
2350-
2351-
2352-class ControlPanelTestCase(ControlPanelMixinTestCase):
2353+ """Max size is not bigger than 736x525 (LP: #645526, LP: #683164)."""
2354+ self.assertTrue(self.ui.get_size_request() <= (736, 525))
2355+
2356+
2357+class ControlPanelTestCase(BaseTestCase):
2358 """The test suite for the control panel itself."""
2359
2360 klass = gui.ControlPanel
2361 kwargs = {'window_id': 7}
2362- ui_filename = 'controlpanel.ui'
2363-
2364- def test_is_a_vbox(self):
2365+
2366+ def assert_current_tab_correct(self, expected_tab):
2367+ """Check that the wiget 'expected_tab' is the current page."""
2368+ actual = self.ui.get_nth_page(self.ui.get_current_page())
2369+ self.assertTrue(expected_tab is actual)
2370+
2371+ def test_is_a_notebook(self):
2372 """Inherits from gtk.VBox."""
2373- self.assertIsInstance(self.ui, gui.gtk.VBox)
2374+ self.assertIsInstance(self.ui, gui.gtk.Notebook)
2375
2376 def test_startup_visibility(self):
2377 """The widget is visible at startup."""
2378- self.assertTrue(self.ui.itself.get_visible(),
2379+ self.assertTrue(self.ui.get_visible(),
2380 'must be visible at startup.')
2381
2382+ def test_startup_props(self):
2383+ """The tabs and border are not shown."""
2384+ self.assertFalse(self.ui.get_show_border(), 'must not show border.')
2385+ self.assertFalse(self.ui.get_show_tabs(), 'must not show tabs.')
2386+
2387 def test_overview_is_shown_at_startup(self):
2388 """The overview is shown at startup."""
2389 self.assertIsInstance(self.ui.overview, gui.OverviewPanel)
2390+ self.assert_current_tab_correct(self.ui.overview)
2391
2392 def test_window_id_is_passed_to_child(self):
2393 """The child gets the window_id."""
2394@@ -299,8 +330,48 @@
2395 def test_on_show_management_panel(self):
2396 """A ManagementPanel is shown when the callback is executed."""
2397 self.ui.on_show_management_panel()
2398- children = self.ui.get_children()
2399- self.assertIsInstance(children[-1], gui.ManagementPanel)
2400+ self.assert_current_tab_correct(self.ui.management)
2401+
2402+ def test_on_show_management_panel_is_idempotent(self):
2403+ """Only one ManagementPanel is shown."""
2404+ self.ui.on_show_management_panel()
2405+ self.ui.on_show_management_panel()
2406+
2407+ self.assert_current_tab_correct(self.ui.management)
2408+
2409+ def test_credentials_found_shows_account_management_panel(self):
2410+ """On 'credentials-found' signal, the management panel is shown.
2411+
2412+ If first signal parameter is False, visible tab should be account.
2413+
2414+ """
2415+ self.patch(self.ui.management, 'load', self._set_called)
2416+ self.ui.overview.emit('credentials-found', False, object())
2417+
2418+ self.assert_current_tab_correct(self.ui.management)
2419+ self.assertEqual(self.ui.management.notebook.get_current_page(),
2420+ self.ui.management.ACCOUNT_PAGE)
2421+ self.assertEqual(self._called, ((), {}))
2422+
2423+ def test_credentials_found_shows_folders_management_panel(self):
2424+ """On 'credentials-found' signal, the management panel is shown.
2425+
2426+ If first signal parameter is True, visible tab should be folders.
2427+
2428+ """
2429+ a_token = object()
2430+ self.ui.overview.emit('credentials-found', True, a_token)
2431+
2432+ self.assert_current_tab_correct(self.ui.management)
2433+ self.assertEqual(self.ui.management.notebook.get_current_page(),
2434+ self.ui.management.FOLDERS_PAGE)
2435+
2436+ def test_local_device_removed_shows_overview_panel(self):
2437+ """On 'local-device-removed' signal, the overview panel is shown."""
2438+ self.ui.overview.emit('credentials-found', True, object())
2439+ self.ui.management.emit('local-device-removed')
2440+
2441+ self.assert_current_tab_correct(self.ui.overview)
2442
2443
2444 class UbuntuOneBinTestCase(BaseTestCase):
2445@@ -334,6 +405,37 @@
2446 ui = self.klass() # no title given
2447 self.assertEqual(ui.title.label.get_text(), '')
2448
2449+ def test_message_is_a_label_loading(self):
2450+ """Message is the correct widget."""
2451+ self.assertIsInstance(self.ui.message, gui.LabelLoading)
2452+ self.assertIn(self.ui.message, self.ui.get_children())
2453+
2454+ def test_on_success(self):
2455+ """Callback to stop the Loading and clear messages."""
2456+ self.ui.on_success()
2457+ self.assertEqual(self.ui.message.get_label(), '')
2458+ self.assertFalse(self.ui.message.active)
2459+
2460+ def test_on_success_with_message(self):
2461+ """Callback to stop the Loading and show a info message."""
2462+ msg = 'WOW! <i>this rocks</i>'
2463+ self.ui.on_success(message=msg)
2464+ self.assertEqual(self.ui.message.get_label(), msg)
2465+ self.assertFalse(self.ui.message.active)
2466+
2467+ def test_on_error(self):
2468+ """Callback to stop the Loading and clear messages."""
2469+ self.ui.on_error()
2470+ self.assert_warning_correct(self.ui.message, gui.VALUE_ERROR)
2471+ self.assertFalse(self.ui.message.active)
2472+
2473+ def test_on_error_with_message(self):
2474+ """Callback to stop the Loading and show a info message."""
2475+ msg = 'WOW! <i>this does not rock</i> :-/'
2476+ self.ui.on_error(message=msg)
2477+ self.assert_warning_correct(self.ui.message, msg)
2478+ self.assertFalse(self.ui.message.active)
2479+
2480
2481 class OverwiewPanelTestCase(ControlPanelMixinTestCase):
2482 """The test suite for the overview panel."""
2483@@ -418,6 +520,7 @@
2484
2485 def test_find_credentials_is_called(self):
2486 """Credentials are asked to SSO backend."""
2487+ self.assertFalse(self.ui._credentials_are_new)
2488 self.assert_backend_called('find_credentials', (gui.U1_APP_NAME, {}),
2489 backend=self.ui.sso_backend)
2490
2491@@ -427,13 +530,28 @@
2492
2493 self.ui.on_credentials_found(gui.U1_APP_NAME, TOKEN)
2494
2495- self.assertFalse(self.ui.get_visible())
2496- self.assertEqual(self._called, ((self.ui, gui.U1_APP_NAME, TOKEN), {}))
2497+ self.assertFalse(self.ui.get_property('greyed'), 'Must not be greyed.')
2498+ # assume credentials were in local keyring
2499+ self.assertEqual(self._called, ((self.ui, False, TOKEN), {}))
2500+
2501+ def test_on_credentials_found_when_creds_are_not_new(self):
2502+ """Callback 'on_credentials_found' distinguish if creds are new."""
2503+ self.ui.connect('credentials-found', self._set_called)
2504+
2505+ # credentials weren't in the system
2506+ self.ui.on_credentials_not_found(gui.U1_APP_NAME)
2507+ # now they are!
2508+ self.ui.on_credentials_found(gui.U1_APP_NAME, TOKEN)
2509+
2510+ self.assertFalse(self.ui.get_property('greyed'), 'Must not be greyed.')
2511+ # assume credentials were not in local keyring
2512+ self.assertEqual(self._called, ((self.ui, True, TOKEN), {}))
2513
2514 def test_on_credentials_not_found(self):
2515 """Callback 'on_credentials_not_found' is correct."""
2516 self.ui.on_credentials_not_found(gui.U1_APP_NAME)
2517 self.assertTrue(self.ui.get_visible())
2518+ self.assertTrue(self.ui._credentials_are_new)
2519
2520 def test_on_credentials_error(self):
2521 """Callback 'on_credentials_error' is correct."""
2522@@ -626,11 +744,11 @@
2523
2524 def assert_account_info_correct(self, info):
2525 """Check that the displayed account info matches 'info'."""
2526- self.assertEqual(self.ui.name_label.get_text(),
2527+ self.assertEqual(self.ui.name_label.get_label(),
2528 FAKE_ACCOUNT_INFO['name'])
2529- self.assertEqual(self.ui.type_label.get_text(),
2530+ self.assertEqual(self.ui.type_label.get_label(),
2531 FAKE_ACCOUNT_INFO['type'])
2532- self.assertEqual(self.ui.email_label.get_text(),
2533+ self.assertEqual(self.ui.email_label.get_label(),
2534 FAKE_ACCOUNT_INFO['email'])
2535
2536 def test_is_an_ubuntuone_bin(self):
2537@@ -645,15 +763,9 @@
2538 """Is visible."""
2539 self.assertTrue(self.ui.get_visible())
2540
2541- def test_type_label_is_loading(self):
2542- """Placeholder for type label is a Loading widget."""
2543- self.assertIsInstance(self.ui.type_label, gui.LabelLoading)
2544- self.assertIn(self.ui.type_label, self.ui.type_box.get_children())
2545-
2546- def test_email_label_is_loading(self):
2547- """Placeholder for email label is a Loading widget."""
2548- self.assertIsInstance(self.ui.email_label, gui.LabelLoading)
2549- self.assertIn(self.ui.email_label, self.ui.email_box.get_children())
2550+ def test_account_info_is_not_visible(self):
2551+ """Account info is not visible."""
2552+ self.assertFalse(self.ui.account.get_visible())
2553
2554 def test_backend_signals(self):
2555 """The proper signals are connected to the backend."""
2556@@ -666,18 +778,16 @@
2557 """The account info is processed when ready."""
2558 self.ui.on_account_info_ready(FAKE_ACCOUNT_INFO)
2559 self.assert_account_info_correct(FAKE_ACCOUNT_INFO)
2560-
2561- for widget in (self.ui.name_label, self.ui.type_label,
2562- self.ui.email_label):
2563- self.assertFalse(widget.active)
2564+ self.assertTrue(self.ui.account.get_visible())
2565+ self.assertFalse(self.ui.message.active)
2566+ self.assertEqual(self.ui.message.get_label(), '')
2567
2568 def test_on_account_info_error(self):
2569 """The account info couldn't be retrieved."""
2570 self.ui.on_account_info_error()
2571- for widget in (self.ui.name_label, self.ui.type_label,
2572- self.ui.email_label):
2573- self.assert_warning_correct(widget, self.ui.VALUE_ERROR)
2574- self.assertFalse(widget.active)
2575+ self.assertFalse(self.ui.account.get_visible())
2576+ self.assertFalse(self.ui.message.active)
2577+ self.assert_warning_correct(self.ui.message, gui.VALUE_ERROR)
2578
2579
2580 class FoldersTestCase(ControlPanelMixinTestCase):
2581@@ -686,6 +796,10 @@
2582 klass = gui.FoldersPanel
2583 ui_filename = 'folders.ui'
2584
2585+ def setUp(self):
2586+ super(FoldersTestCase, self).setUp()
2587+ self.ui.load()
2588+
2589 def test_is_an_ubuntuone_bin(self):
2590 """Inherits from UbuntuOneBin."""
2591 self.assertIsInstance(self.ui, gui.UbuntuOneBin)
2592@@ -705,34 +819,393 @@
2593 self.assertEqual(self.ui.backend._signals['VolumesInfoError'],
2594 [self.ui.on_volumes_info_error])
2595
2596- def test_volumes_info_is_requested(self):
2597+ def test_volumes_info_is_requested_on_load(self):
2598 """The volumes info is requested to the backend."""
2599+ # clean backend calls
2600+ self.ui.backend._called.pop('volumes_info', None)
2601+ self.ui.load()
2602+
2603 self.assert_backend_called('volumes_info', ())
2604
2605- def _test_on_volumes_info_ready(self):
2606+ def test_message_after_load(self):
2607+ """The volumes label is active when contents are load."""
2608+ self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2609+ self.ui.load()
2610+
2611+ self.assertTrue(self.ui.message.active)
2612+
2613+ def test_message_after_non_empty_volumes_info_ready(self):
2614+ """The volumes label is a LabelLoading."""
2615+ self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2616+
2617+ self.assertFalse(self.ui.message.active)
2618+
2619+ def test_message_after_empty_volumes_info_ready(self):
2620+ """When there are no volumes, a notification is shown."""
2621+ self.ui.on_volumes_info_ready([])
2622+
2623+ self.assertFalse(self.ui.message.active)
2624+ self.assertEqual(self.ui.message.get_text(), self.ui.NO_VOLUMES)
2625+
2626+ def test_on_volumes_info_ready(self):
2627 """The volumes info is processed when ready."""
2628 self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2629
2630- volumes = self.ui.itself.get_children()
2631+ self.assertEqual(self.ui.folders.get_children(), [self.ui.volumes])
2632+
2633+ volumes = self.ui.volumes.get_children()
2634+ volumes.reverse()
2635+
2636+ header = volumes[:2] # grab header
2637+ self.assertEqual(header[0].get_text(), 'Local path')
2638+ self.assertEqual(header[1].get_text(), 'Subscribed')
2639+
2640+ volumes = volumes[2:] # drop header
2641+ labels = filter(lambda w: isinstance(w, gui.gtk.Label), volumes)
2642+ checks = filter(lambda w: isinstance(w, gui.gtk.CheckButton), volumes)
2643+
2644+ self.assertEqual(len(checks), len(FAKE_VOLUMES_INFO))
2645+
2646+ for label, check, volume in zip(labels, checks, FAKE_VOLUMES_INFO):
2647+ self.assertEqual(volume['suggested_path'], label.get_text())
2648+ self.assertEqual(bool(volume['subscribed']), check.get_active())
2649+ self.assertEqual(volume['volume_id'], check.get_label())
2650+ self.assertFalse(check.get_child().get_visible())
2651+
2652+ def test_on_volumes_info_ready_clears_the_list(self):
2653+ """The old volumes info is cleared before updated."""
2654+ self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2655+ self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2656+
2657+ self.assertEqual(len(self.ui.folders.get_children()), 1)
2658+ child = self.ui.folders.get_children()[0]
2659+ self.assertEqual(child, self.ui.volumes)
2660+
2661+ volumes = filter(lambda w: isinstance(w, gui.gtk.CheckButton),
2662+ self.ui.volumes.get_children())
2663 self.assertEqual(len(volumes), len(FAKE_VOLUMES_INFO))
2664
2665- for i, volume in enumerate(FAKE_VOLUMES_INFO):
2666- vol_widget = volumes[i]
2667- self.assertEqual(vol_widget.id, volume['volume_id'])
2668- self.assertEqual(vol_widget.path.get_text(), volume['path'])
2669- subscribed = gui.dbus_str_to_bool(volume['subscribed'])
2670- self.assertEqual(vol_widget.subscribed.get_active(), subscribed)
2671+ def test_on_volumes_info_ready_with_no_volumes(self):
2672+ """When there are no volumes, a notification is shown."""
2673+ self.ui.on_volumes_info_ready([])
2674+ # no volumes table
2675+ self.assertEqual(len(self.ui.folders.get_children()), 0)
2676+ self.assertTrue(self.ui.volumes is None)
2677+
2678+ def test_on_subscribed_clicked(self):
2679+ """Clicking on 'subscribed' updates the folder subscription."""
2680+ self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2681+
2682+ method = 'change_volume_settings'
2683+ for checkbutton in self.ui._subscribed:
2684+ checkbutton.clicked()
2685+ fid = checkbutton.get_label()
2686+
2687+ subscribed = gui.bool_str(checkbutton.get_active())
2688+ self.assert_backend_called(method,
2689+ (fid, {'subscribed': subscribed}))
2690+ # clean backend calls
2691+ self.ui.backend._called.pop(method)
2692+
2693+ checkbutton.clicked()
2694+ subscribed = gui.bool_str(checkbutton.get_active())
2695+ self.assert_backend_called('change_volume_settings',
2696+ (fid, {'subscribed': subscribed}))
2697
2698 def test_on_volumes_info_error(self):
2699 """The volumes info couldn't be retrieved."""
2700 self.ui.on_volumes_info_error()
2701-
2702- def _test_on_volumes_info_ready_clears_the_list(self):
2703- """The old volumes info is cleared before updated."""
2704- self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2705- self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2706- volumes = self.ui.itself.get_children()
2707- self.assertEqual(len(volumes), len(FAKE_VOLUMES_INFO))
2708+ self.assert_warning_correct(warning=self.ui.message,
2709+ text=gui.VALUE_ERROR)
2710+ self.assertFalse(self.ui.message.active)
2711+
2712+ def test_on_volumes_info_error_after_success(self):
2713+ """The volumes info couldn't be retrieved after a prior success."""
2714+ self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
2715+
2716+ self.ui.on_volumes_info_error()
2717+
2718+ self.test_on_volumes_info_error()
2719+ self.test_on_volumes_info_ready_with_no_volumes()
2720+
2721+
2722+class DeviceTestCase(ControlPanelMixinTestCase):
2723+ """The test suite for the device widget."""
2724+
2725+ klass = gui.Device
2726+ ui_filename = 'device.ui'
2727+
2728+ def assert_device_equal(self, device, expected):
2729+ """Assert that the device has the values from expected."""
2730+ self.assertEqual(device.id,
2731+ expected['device_id'])
2732+ self.assertEqual(device.device_name.get_text(),
2733+ expected['device_name'])
2734+ self.assertEqual(device.device_type.get_icon_name()[0],
2735+ expected['device_type'].lower())
2736+ self.assertEqual(device.is_local,
2737+ bool(expected['is_local']))
2738+ self.assertEqual(device.configurable,
2739+ bool(expected['configurable']))
2740+ self.assertEqual(device.limit_bandwidth.get_active(),
2741+ bool(expected['limit_bandwidth']))
2742+
2743+ value = int(expected['max_upload_speed']) // gui.KILOBYTES
2744+ self.assertEqual(device.max_upload_speed.get_value_as_int(), value)
2745+ value = int(expected['max_download_speed']) // gui.KILOBYTES
2746+ self.assertEqual(device.max_download_speed.get_value_as_int(), value)
2747+
2748+ def assert_device_settings_changed(self):
2749+ """Changing throttling settings updates the backend properly."""
2750+ expected = self.ui.__dict__
2751+ self.assert_backend_called('change_device_settings',
2752+ (self.ui.id, expected))
2753+ self.assertEqual(self.ui.warning_label.get_text(), '')
2754+
2755+ def modify_settings(self):
2756+ """Modify settings so values actually change."""
2757+ new_val = not self.ui.limit_bandwidth.get_active()
2758+ self.ui.limit_bandwidth.set_active(new_val)
2759+
2760+ new_val = self.ui.max_upload_speed.get_value_as_int() + 1
2761+ self.ui.max_upload_speed.set_value(new_val)
2762+
2763+ new_val = self.ui.max_download_speed.get_value_as_int() + 1
2764+ self.ui.max_download_speed.set_value(new_val)
2765+
2766+ def test_is_a_vbox(self):
2767+ """Inherits from VBox."""
2768+ self.assertIsInstance(self.ui, gui.gtk.VBox)
2769+
2770+ def test_inner_widget_is_packed(self):
2771+ """The 'itself' vbox is packed into the widget."""
2772+ self.assertIn(self.ui.itself, self.ui.get_children())
2773+
2774+ def test_is_visible(self):
2775+ """Is visible."""
2776+ self.assertTrue(self.ui.get_visible())
2777+
2778+ def test_is_sensitive(self):
2779+ """Is sensitive."""
2780+ self.assertTrue(self.ui.get_sensitive())
2781+
2782+ def test_warning_label_is_cleared(self):
2783+ """The warning label is cleared."""
2784+ self.assertEqual(self.ui.warning_label.get_text(), '')
2785+
2786+ def test_default_values(self):
2787+ """Default values are correct."""
2788+ self.assertEqual(self.ui.id, None)
2789+ self.assertEqual(self.ui.device_name.get_text(), '')
2790+ self.assertEqual(self.ui.device_type.get_icon_name()[0],
2791+ gui.DEVICE_TYPE_COMPUTER.lower())
2792+ self.assertEqual(self.ui.is_local, False)
2793+ self.assertEqual(self.ui.configurable, False)
2794+ self.assertEqual(self.ui.limit_bandwidth.get_active(), False)
2795+ self.assertEqual(self.ui.max_upload_speed.get_value_as_int(), 0)
2796+ self.assertEqual(self.ui.max_download_speed.get_value_as_int(), 0)
2797+
2798+ def test_init_does_not_call_backend(self):
2799+ """When updating, the backend is not called."""
2800+ self.assertEqual(self.ui.backend._called, {})
2801+
2802+ def test_update_device_name(self):
2803+ """A device can be updated from a dict."""
2804+ value = 'The death star'
2805+ self.ui.update(device_name=value)
2806+ self.assertEqual(value, self.ui.device_name.get_text())
2807+
2808+ def test_update_unicode_device_name(self):
2809+ """A device can be updated from a dict."""
2810+ value = u'Ñoño Ñandú'
2811+ self.ui.update(device_name=value)
2812+ self.assertEqual(value, self.ui.device_name.get_text())
2813+
2814+ def test_update_device_type_computer(self):
2815+ """A device can be updated from a dict."""
2816+ dtype = gui.DEVICE_TYPE_COMPUTER
2817+ self.ui.update(device_type=dtype)
2818+ self.assertEqual((dtype.lower(), gui.gtk.ICON_SIZE_BUTTON),
2819+ self.ui.device_type.get_icon_name())
2820+
2821+ def test_update_device_type_phone(self):
2822+ """A device can be updated from a dict."""
2823+ dtype = gui.DEVICE_TYPE_PHONE
2824+ self.ui.update(device_type=dtype)
2825+ self.assertEqual((dtype.lower(), gui.gtk.ICON_SIZE_BUTTON),
2826+ self.ui.device_type.get_icon_name())
2827+
2828+ def test_update_is_not_local(self):
2829+ """A device can be updated from a dict."""
2830+ self.ui.update(is_local='')
2831+ self.assertFalse(self.ui.is_local)
2832+
2833+ def test_update_is_local(self):
2834+ """A device can be updated from a dict."""
2835+ self.ui.update(is_local='True')
2836+ self.assertTrue(self.ui.is_local)
2837+
2838+ def test_update_non_configurable(self):
2839+ """A device can be updated from a dict."""
2840+ self.ui.update(configurable='')
2841+ self.assertFalse(self.ui.configurable)
2842+ self.assertFalse(self.ui.throttling.get_visible())
2843+
2844+ def test_update_configurable(self):
2845+ """A device can be updated from a dict."""
2846+ self.ui.update(configurable='True')
2847+ self.assertTrue(self.ui.configurable)
2848+ self.assertTrue(self.ui.throttling.get_visible())
2849+
2850+ def test_update_limit_bandwidth(self):
2851+ """A device can be updated from a dict."""
2852+ self.ui.update(limit_bandwidth='')
2853+ self.assertFalse(self.ui.limit_bandwidth.get_active())
2854+
2855+ self.ui.update(limit_bandwidth='True')
2856+ self.assertTrue(self.ui.limit_bandwidth.get_active())
2857+
2858+ def test_update_upload_speed(self):
2859+ """A device can be updated from a dict."""
2860+ value = '12345'
2861+ self.ui.update(max_upload_speed=value)
2862+ self.assertEqual(int(value) // gui.KILOBYTES,
2863+ self.ui.max_upload_speed.get_value_as_int())
2864+
2865+ def test_update_download_speed(self):
2866+ """A device can be updated from a dict."""
2867+ value = '987654'
2868+ self.ui.update(max_download_speed=value)
2869+ self.assertEqual(int(value) // gui.KILOBYTES,
2870+ self.ui.max_download_speed.get_value_as_int())
2871+
2872+ def test_update_does_not_call_backend(self):
2873+ """When updating, the backend is not called."""
2874+ self.ui.update(**FAKE_DEVICE_INFO)
2875+ self.assertEqual(self.ui.backend._called, {})
2876+ self.assert_device_equal(self.ui, FAKE_DEVICE_INFO)
2877+
2878+ def test_on_limit_bandwidth_toggled(self):
2879+ """When toggling limit_bandwidth, backend is updated."""
2880+ self.ui.limit_bandwidth.toggled()
2881+ self.assert_device_settings_changed()
2882+
2883+ def test_on_max_upload_speed_value_changed(self):
2884+ """When setting max_upload_speed, backend is updated."""
2885+ self.ui.max_upload_speed.set_value(25)
2886+ self.assert_device_settings_changed()
2887+
2888+ def test_on_max_download_speed_value_changed(self):
2889+ """When setting max_download_speed, backend is updated."""
2890+ self.ui.max_download_speed.set_value(52)
2891+ self.assert_device_settings_changed()
2892+
2893+ def test_backend_signals(self):
2894+ """The proper signals are connected to the backend."""
2895+ self.assertEqual(self.ui.backend._signals['DeviceSettingsChanged'],
2896+ [self.ui.on_device_settings_changed])
2897+ self.assertEqual(self.ui.backend._signals['DeviceSettingsChangeError'],
2898+ [self.ui.on_device_settings_change_error])
2899+ self.assertEqual(self.ui.backend._signals['DeviceRemoved'],
2900+ [self.ui.on_device_removed])
2901+ self.assertEqual(self.ui.backend._signals['DeviceRemovalError'],
2902+ [self.ui.on_device_removal_error])
2903+
2904+ def test_on_device_settings_changed(self):
2905+ """When settings were changed for this device, enable it."""
2906+ self.modify_settings()
2907+ self.ui.on_device_settings_changed(device_id=self.ui.id)
2908+
2909+ self.assertTrue(self.ui.get_sensitive())
2910+ self.assertEqual(self.ui.warning_label.get_text(), '')
2911+ self.assertEqual(self.ui.__dict__, self.ui._last_settings)
2912+
2913+ def test_on_device_settings_change_after_error(self):
2914+ """Change success after error."""
2915+ self.modify_settings()
2916+ self.ui.on_device_settings_change_error(device_id=self.ui.id) # error
2917+
2918+ self.test_on_device_settings_changed()
2919+
2920+ def test_on_device_settings_changed_different_id(self):
2921+ """When settings were changed for other device, nothing changes."""
2922+ self.modify_settings()
2923+ self.ui.on_device_settings_changed(device_id='yadda')
2924+
2925+ self.assertEqual(self.ui.warning_label.get_text(), '')
2926+
2927+ def test_on_device_settings_change_error(self):
2928+ """When settings were not changed for this device, notify the user.
2929+
2930+ Also, confirm that old values were restored.
2931+
2932+ """
2933+ self.ui.update(**FAKE_DEVICE_INFO) # use known values
2934+
2935+ self.modify_settings()
2936+
2937+ self.ui.on_device_settings_change_error(device_id=self.ui.id) # error
2938+
2939+ self.assertTrue(self.ui.get_sensitive())
2940+ self.assert_warning_correct(self.ui.warning_label,
2941+ self.ui.DEVICE_CHANGE_ERROR)
2942+ self.assert_device_equal(self.ui, FAKE_DEVICE_INFO) # restored info
2943+
2944+ def test_on_device_settings_change_error_after_success(self):
2945+ """Change error after success."""
2946+ self.modify_settings()
2947+ self.ui.on_device_settings_changed(device_id=self.ui.id)
2948+
2949+ self.test_on_device_settings_change_error()
2950+
2951+ def test_on_device_settings_change_error_different_id(self):
2952+ """When settings were not changed for other device, do nothing."""
2953+ self.modify_settings()
2954+ self.ui.on_device_settings_change_error(device_id='yudo')
2955+ self.assertEqual(self.ui.warning_label.get_text(), '')
2956+
2957+ def test_remove(self):
2958+ """Clicking on remove calls the backend properly."""
2959+ self.ui.is_local = False
2960+ self.ui.remove.clicked()
2961+
2962+ self.assert_backend_called('remove_device', (self.ui.id,))
2963+ self.assertFalse(self.ui.get_sensitive(),
2964+ 'Must be disabled while removing.')
2965+
2966+ def test_on_device_removed(self):
2967+ """On this device removed, hide and destroy."""
2968+ self.ui.remove.clicked()
2969+ self.ui.on_device_removed(device_id=self.ui.id)
2970+
2971+ self.assertFalse(self.ui.get_visible(),
2972+ 'Must not be visible after removed.')
2973+
2974+ def test_on_device_removed_other_id(self):
2975+ """On other device removed, do nothing."""
2976+ self.ui.remove.clicked()
2977+ self.ui.on_device_removed(device_id='foo')
2978+
2979+ self.assertTrue(self.ui.get_visible(),
2980+ 'Must be visible after other device was removed.')
2981+
2982+ def test_on_device_removal_error(self):
2983+ """On this device removal error, re-enable and show error."""
2984+ self.ui.remove.clicked()
2985+ self.ui.on_device_removal_error(device_id=self.ui.id)
2986+
2987+ self.assertTrue(self.ui.get_sensitive(),
2988+ 'Must be enabled after removal error.')
2989+ self.assert_warning_correct(self.ui.warning_label,
2990+ self.ui.DEVICE_REMOVAL_ERROR)
2991+
2992+ def test_on_device_removal_error_other_id(self):
2993+ """On other device removal error, do nothing."""
2994+ self.ui.remove.clicked()
2995+ self.ui.on_device_removal_error(device_id='foo')
2996+
2997+ self.assertFalse(self.ui.get_sensitive(),
2998+ 'Must be disabled after other device removal error.')
2999
3000
3001 class DevicesTestCase(ControlPanelMixinTestCase):
3002@@ -753,6 +1226,144 @@
3003 """Is visible."""
3004 self.assertTrue(self.ui.get_visible())
3005
3006+ def test_backend_signals(self):
3007+ """The proper signals are connected to the backend."""
3008+ self.assertEqual(self.ui.backend._signals['DevicesInfoReady'],
3009+ [self.ui.on_devices_info_ready])
3010+ self.assertEqual(self.ui.backend._signals['DevicesInfoError'],
3011+ [self.ui.on_devices_info_error])
3012+ self.assertEqual(self.ui.backend._signals['DeviceRemoved'],
3013+ [self.ui.on_device_removed])
3014+
3015+ def test_devices_info_is_requested_on_load(self):
3016+ """The devices info is requested to the backend."""
3017+ # clean backend calls
3018+ self.ui.backend._called.pop('devices_info', None)
3019+ self.ui.load()
3020+
3021+ self.assert_backend_called('devices_info', ())
3022+
3023+ def test_message_after_load(self):
3024+ """The devices label is active when contents are load."""
3025+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3026+ self.ui.load()
3027+
3028+ self.assertTrue(self.ui.message.active)
3029+
3030+ def test_message_after_non_empty_devices_info_ready(self):
3031+ """The devices label is a LabelLoading."""
3032+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3033+
3034+ self.assertFalse(self.ui.message.active)
3035+
3036+ def test_message_after_empty_devices_info_ready(self):
3037+ """When there are no devices, a notification is shown."""
3038+ self.ui.on_devices_info_ready([])
3039+
3040+ self.assertFalse(self.ui.message.active)
3041+ self.assertEqual(self.ui.message.get_text(), self.ui.NO_DEVICES)
3042+
3043+ def test_on_devices_info_ready(self):
3044+ """The devices info is processed when ready."""
3045+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3046+
3047+ children = self.ui.devices.get_children()
3048+ self.assertEqual(len(children), len(FAKE_DEVICES_INFO))
3049+
3050+ for child, device in zip(children, FAKE_DEVICES_INFO):
3051+ self.assertIsInstance(child, gui.Device)
3052+
3053+ self.assertEqual(device['device_id'], child.id)
3054+ self.assertEqual(device['device_name'],
3055+ child.device_name.get_text())
3056+ self.assertEqual(device['device_type'].lower(),
3057+ child.device_type.get_icon_name()[0])
3058+ self.assertEqual(bool(device['is_local']),
3059+ child.is_local)
3060+ self.assertEqual(bool(device['configurable']),
3061+ child.configurable)
3062+
3063+ if bool(device['configurable']):
3064+ self.assertEqual(bool(device['limit_bandwidth']),
3065+ child.limit_bandwidth.get_active())
3066+ value = int(device['max_upload_speed']) // gui.KILOBYTES
3067+ self.assertEqual(value,
3068+ child.max_upload_speed.get_value_as_int())
3069+ value = int(device['max_download_speed']) // gui.KILOBYTES
3070+ self.assertEqual(value,
3071+ child.max_download_speed.get_value_as_int())
3072+
3073+ def test_on_devices_info_ready_have_devices_cached(self):
3074+ """The devices are cached for further removal."""
3075+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3076+
3077+ for child in self.ui.devices.get_children():
3078+ self.assertTrue(self.ui._devices[child.id] is child)
3079+
3080+ def test_on_devices_info_ready_clears_the_list(self):
3081+ """The old devices info is cleared before updated."""
3082+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3083+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3084+
3085+ devices = self.ui.devices.get_children()
3086+ self.assertEqual(len(devices), len(FAKE_DEVICES_INFO))
3087+
3088+ def test_on_devices_info_ready_with_no_devices(self):
3089+ """When there are no devices, a notification is shown."""
3090+ self.ui.on_devices_info_ready([])
3091+ self.assertEqual(len(self.ui.devices.get_children()), 0)
3092+
3093+ def test_on_devices_info_error(self):
3094+ """The devices info couldn't be retrieved."""
3095+ self.ui.on_devices_info_error()
3096+
3097+ self.assert_warning_correct(warning=self.ui.message,
3098+ text=gui.VALUE_ERROR)
3099+ self.assertFalse(self.ui.message.active)
3100+
3101+ def test_on_devices_info_error_after_success(self):
3102+ """The devices info couldn't be retrieved after a prior success."""
3103+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3104+
3105+ self.ui.on_devices_info_error()
3106+
3107+ self.test_on_devices_info_error()
3108+ self.test_on_devices_info_ready_with_no_devices()
3109+
3110+ def test_on_device_removed(self):
3111+ """When a child device was removed, remove and destroy."""
3112+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3113+ did = FAKE_DEVICES_INFO[0]['device_id']
3114+ device = self.ui._devices[did]
3115+ self.ui.on_device_removed(device_id=did)
3116+
3117+ self.assertTrue(device not in self.ui.devices.get_children())
3118+ self.assertTrue(did not in self.ui._devices)
3119+
3120+ def test_on_local_device_removed(self):
3121+ """Removing the local device emits local-device-removed."""
3122+ self.ui.connect('local-device-removed', self._set_called)
3123+
3124+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3125+ local_device = FAKE_DEVICES_INFO[-1]
3126+ assert bool(local_device['is_local'])
3127+ local_device_id = local_device['device_id']
3128+ assert self.ui._devices[local_device_id].is_local
3129+
3130+ self.ui.on_device_removed(device_id=local_device_id)
3131+
3132+ self.assertEqual(self._called, ((self.ui,), {}))
3133+
3134+ def test_on_device_removed_for_no_child_device(self):
3135+ """On other device removed, do nothing."""
3136+ self.ui.on_devices_info_ready(FAKE_DEVICES_INFO)
3137+ old_devices = self.ui.devices.get_children()
3138+
3139+ self.ui.on_device_removed(device_id='foo')
3140+
3141+ new_devices = self.ui.devices.get_children()
3142+ self.assertEqual(new_devices, old_devices)
3143+
3144
3145 class ApplicationsTestCase(ControlPanelMixinTestCase):
3146 """The test suite for the applications panel."""
3147@@ -842,15 +1453,16 @@
3148 class ManagementPanelAccountTestCase(ManagementPanelTestCase):
3149 """The test suite for the management panel (account tab)."""
3150
3151- def test_backend_signals(self):
3152+ def test_backend_account_signals(self):
3153 """The proper signals are connected to the backend."""
3154 self.assertEqual(self.ui.backend._signals['AccountInfoReady'],
3155 [self.ui.on_account_info_ready])
3156 self.assertEqual(self.ui.backend._signals['AccountInfoError'],
3157 [self.ui.on_account_info_error])
3158
3159- def test_account_info_is_requested(self):
3160+ def test_account_info_is_requested_on_load(self):
3161 """The account info is requested to the backend."""
3162+ self.ui.load()
3163 self.assert_backend_called('account_info', ())
3164
3165 def test_account_panel_is_packed(self):
3166@@ -877,12 +1489,31 @@
3167 actual = self.ui.notebook.get_nth_page(self.ui.APPLICATIONS_PAGE)
3168 self.assertTrue(self.ui.applications is actual)
3169
3170- def test_placeholders_are_loading(self):
3171- """Placeholders for labels are a Loading widget."""
3172- widgets = (self.ui.quota_label, self.ui.status_label)
3173- for widget in widgets:
3174- self.assertIsInstance(widget, gui.LabelLoading)
3175- self.assertIn(widget, self.ui.status_box.get_children())
3176+ def test_entering_folders_tab_loads_content(self):
3177+ """The volumes info is loaded when entering the Folders tab."""
3178+ self.patch(self.ui.folders, 'load', self._set_called)
3179+ # clean backend calls
3180+ self.ui.folders_button.clicked()
3181+
3182+ self.assertEqual(self._called, ((), {}))
3183+
3184+ def test_entering_devices_tab_loads_content(self):
3185+ """The devices info is loaded when entering the Devices tab."""
3186+ self.patch(self.ui.devices, 'load', self._set_called)
3187+ # clean backend calls
3188+ self.ui.devices_button.clicked()
3189+
3190+ self.assertEqual(self._called, ((), {}))
3191+
3192+ def test_quota_placeholder_is_loading(self):
3193+ """Placeholders for quota label is a Loading widget."""
3194+ self.assertIsInstance(self.ui.quota_label, gui.LabelLoading)
3195+ self.assertIn(self.ui.quota_label, self.ui.quota_box.get_children())
3196+
3197+ def test_file_sync_status_placeholder_is_loading(self):
3198+ """Placeholders for file sync label is a Loading widget."""
3199+ self.assertIsInstance(self.ui.status_label, gui.LabelLoading)
3200+ self.assertIn(self.ui.status_label, self.ui.status_box.get_children())
3201
3202 def test_on_account_info_ready(self):
3203 """The account info is processed when ready."""
3204@@ -896,5 +1527,80 @@
3205 """The account info couldn't be retrieved."""
3206 self.ui.on_account_info_error()
3207 for widget in (self.ui.quota_label,):
3208- self.assert_warning_correct(widget, self.ui.VALUE_ERROR)
3209+ self.assert_warning_correct(widget, gui.VALUE_ERROR)
3210 self.assertFalse(widget.active)
3211+
3212+ def test_backend_file_sync_signals(self):
3213+ """The proper signals are connected to the backend."""
3214+ matches = (
3215+ ('FileSyncStatusDisabled', [self.ui.on_file_sync_status_disabled]),
3216+ ('FileSyncStatusStarting', [self.ui.on_file_sync_status_starting]),
3217+ ('FileSyncStatusDisconnected',
3218+ [self.ui.on_file_sync_status_disconnected]),
3219+ ('FileSyncStatusSyncing', [self.ui.on_file_sync_status_syncing]),
3220+ ('FileSyncStatusIdle', [self.ui.on_file_sync_status_idle]),
3221+ ('FileSyncStatusError', [self.ui.on_file_sync_status_error]),
3222+ )
3223+ for sig, handlers in matches:
3224+ self.assertEqual(self.ui.backend._signals[sig], handlers)
3225+
3226+ def test_file_sync_status_is_requested_on_load(self):
3227+ """The file sync status is requested to the backend."""
3228+ self.ui.load()
3229+ self.assert_backend_called('file_sync_status', ())
3230+
3231+ def test_on_file_sync_status_disabled(self):
3232+ """The file sync is disabled."""
3233+ self.ui.on_file_sync_status_disabled('msg')
3234+
3235+ self.assertFalse(self.ui.status_label.active)
3236+ self.assertEqual(self.ui.status_label.get_text(),
3237+ self.ui.FILE_SYNC_DISABLED)
3238+
3239+ def test_on_file_sync_status_starting(self):
3240+ """The file sync status is starting."""
3241+ self.ui.on_file_sync_status_starting('msg')
3242+
3243+ self.assertFalse(self.ui.status_label.active)
3244+ self.assertEqual(self.ui.status_label.get_text(),
3245+ self.ui.FILE_SYNC_STARTING)
3246+
3247+ def test_on_file_sync_status_disconnected(self):
3248+ """The file sync status is disconnected."""
3249+ self.ui.on_file_sync_status_disconnected('msg')
3250+
3251+ self.assertFalse(self.ui.status_label.active)
3252+ self.assertEqual(self.ui.status_label.get_text(),
3253+ self.ui.FILE_SYNC_DISCONNECTED)
3254+
3255+ def test_on_file_sync_status_syncing(self):
3256+ """The file sync status is syncing."""
3257+ self.ui.on_file_sync_status_syncing('msg')
3258+
3259+ self.assertFalse(self.ui.status_label.active)
3260+ self.assertEqual(self.ui.status_label.get_text(),
3261+ self.ui.FILE_SYNC_SYNCING)
3262+
3263+ def test_on_file_sync_status_idle(self):
3264+ """The file sync status is idle."""
3265+ self.ui.on_file_sync_status_idle('msg')
3266+
3267+ self.assertFalse(self.ui.status_label.active)
3268+ self.assertEqual(self.ui.status_label.get_text(),
3269+ self.ui.FILE_SYNC_IDLE)
3270+
3271+ def test_on_file_sync_status_error(self):
3272+ """The file sync status couldn't be retrieved."""
3273+ self.ui.on_file_sync_status_error({'error_msg': 'error msg'})
3274+
3275+ self.assert_warning_correct(self.ui.status_label,
3276+ self.ui.FILE_SYNC_ERROR)
3277+ self.assertFalse(self.ui.status_label.active)
3278+
3279+ def test_local_device_removed_is_emitted(self):
3280+ """Signal local-device-removed is sent when DevicesPanel emits it."""
3281+ self.ui.connect('local-device-removed', self._set_called)
3282+
3283+ self.ui.devices.emit('local-device-removed')
3284+
3285+ self.assertEqual(self._called, ((self.ui,), {}))
3286
3287=== modified file 'ubuntuone/controlpanel/gtk/tests/test_widgets.py'
3288--- ubuntuone/controlpanel/gtk/tests/test_widgets.py 2010-12-06 12:27:11 +0000
3289+++ ubuntuone/controlpanel/gtk/tests/test_widgets.py 2010-12-22 14:37:52 +0000
3290@@ -75,33 +75,26 @@
3291 def test_creation(self):
3292 """A LabelLoading can be created."""
3293 self.assertEqual(self.widget.label.get_text(), '')
3294- self.assertFalse(self.widget.label.get_visible())
3295+ self.assertTrue(self.widget.label.get_visible())
3296
3297 self.assertIsInstance(self.widget.loading, widgets.Loading)
3298 self.assertTrue(self.widget.loading.get_visible())
3299- self.assertTrue(self.widget.active)
3300+ self.assertTrue(self.widget.active) # loading label is packed
3301
3302 def test_stop(self):
3303 """Stop hides the Loading widget."""
3304 self.widget.stop()
3305- self.assertTrue(self.widget.label.get_visible())
3306- self.assertFalse(self.widget.loading.get_visible())
3307+
3308+ self.assertTrue(self.widget.get_child() is self.widget.label)
3309 self.assertFalse(self.widget.active)
3310
3311 def test_start(self):
3312 """Start shows the Loading widget."""
3313 self.widget.start()
3314- self.assertFalse(self.widget.label.get_visible())
3315- self.assertTrue(self.widget.loading.get_visible())
3316+
3317+ self.assertTrue(self.widget.get_child() is self.widget.loading)
3318 self.assertTrue(self.widget.active)
3319
3320- def test_children(self):
3321- """A LabelLoading have proper children."""
3322- children = self.widget.get_children()
3323- self.assertEqual(2, len(children))
3324- self.assertTrue(self.widget.label is children[0])
3325- self.assertTrue(self.widget.loading is children[-1])
3326-
3327 def test_get_text(self):
3328 """Text can be queried."""
3329 text = 'Test me.'
3330
3331=== modified file 'ubuntuone/controlpanel/gtk/widgets.py'
3332--- ubuntuone/controlpanel/gtk/widgets.py 2010-12-06 12:27:11 +0000
3333+++ ubuntuone/controlpanel/gtk/widgets.py 2010-12-22 14:37:52 +0000
3334@@ -49,7 +49,7 @@
3335 self.show_all()
3336
3337
3338-class LabelLoading(gtk.HBox):
3339+class LabelLoading(gtk.Alignment):
3340 """A spinner and a label."""
3341
3342 def __init__(self, loading_label, fg_color=None, *args, **kwargs):
3343@@ -57,29 +57,36 @@
3344 self.loading = Loading(loading_label, fg_color=fg_color)
3345
3346 self.label = gtk.Label()
3347+ self.label.show()
3348 if fg_color is not None:
3349 self.label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.Color(fg_color))
3350
3351- self.pack_start(self.label, expand=False)
3352- self.pack_start(self.loading, expand=False)
3353+ self.add(self.loading)
3354
3355 self.show()
3356+ self.set(xalign=0.5, yalign=0.5, xscale=0, yscale=0)
3357+ self.set_padding(padding_top=5, padding_bottom=0,
3358+ padding_left=5, padding_right=5)
3359 self.start()
3360
3361 @property
3362 def active(self):
3363 """Whether the Loading widget is visible or not."""
3364- return not self.label.get_visible() and self.loading.get_visible()
3365+ return self.get_child() is self.loading
3366
3367 def start(self):
3368 """Show the Loading instead of the Label widget."""
3369- self.label.hide()
3370- self.loading.show()
3371+ for child in self.get_children():
3372+ self.remove(child)
3373+
3374+ self.add(self.loading)
3375
3376 def stop(self):
3377 """Show the label instead of the Loading widget."""
3378- self.label.show()
3379- self.loading.hide()
3380+ for child in self.get_children():
3381+ self.remove(child)
3382+
3383+ self.add(self.label)
3384
3385 def set_text(self, text):
3386 """Set 'text' to be the label's text."""
3387
3388=== modified file 'ubuntuone/controlpanel/integrationtests/__init__.py'
3389--- ubuntuone/controlpanel/integrationtests/__init__.py 2010-12-06 12:27:11 +0000
3390+++ ubuntuone/controlpanel/integrationtests/__init__.py 2010-12-22 14:37:52 +0000
3391@@ -43,8 +43,13 @@
3392 def setUp(self):
3393 super(DBusClientTestCase, self).setUp()
3394 self.mock = None
3395+ self._called = False
3396 dbus_service.init_mainloop()
3397
3398+ def _set_called(self, *args, **kwargs):
3399+ """Keep track of function calls, useful for monkeypatching."""
3400+ self._called = (args, kwargs)
3401+
3402 def register_mockserver(self, bus_name, object_path, object_class,
3403 **kwargs):
3404 """The mock service is registered on the DBus."""
3405
3406=== modified file 'ubuntuone/controlpanel/integrationtests/test_dbus_client_sd.py'
3407--- ubuntuone/controlpanel/integrationtests/test_dbus_client_sd.py 2010-12-06 12:27:11 +0000
3408+++ ubuntuone/controlpanel/integrationtests/test_dbus_client_sd.py 2010-12-22 14:37:52 +0000
3409@@ -204,13 +204,13 @@
3410
3411
3412 class FoldersMockDBusSyncDaemon(sd_dbus_iface.Folders):
3413- """A mock object that mimicks syncdaemon regarding the folders iface."""
3414+ """A mock object that mimicks syncdaemon regarding the Folders iface."""
3415
3416 # __init__ method from a non direct base class 'Object' is called
3417 # __init__ method from base class 'Folders' is not called
3418 # pylint: disable=W0231, W0233
3419
3420- def __init__(self, object_path, conn, vm=None):
3421+ def __init__(self, object_path, conn):
3422 self.udfs = {}
3423 self.udf_id = 1
3424 dbus.service.Object.__init__(self,
3425@@ -220,9 +220,15 @@
3426 def _new_udf(cls, uid, path, subscribed=False):
3427 """Create a new faked udf."""
3428 udf = {}
3429- udf['path'] = udf['suggested_path'] = path
3430- udf['id'] = uid
3431- udf['subscribed'] = subscribed
3432+ if isinstance(path, str):
3433+ path = path.decode('utf-8')
3434+ udf[u'path'] = udf[u'suggested_path'] = path
3435+ udf[u'volume_id'] = unicode(uid)
3436+ udf[u'subscribed'] = sd_dbus_iface.bool_str(subscribed)
3437+ udf[u'node_id'] = u'a18f4cbd-a846-4405-aaa1-b28904817089'
3438+ udf[u'generation'] = u''
3439+ udf[u'type'] = u'UDF'
3440+
3441 return udf
3442
3443 @dbus.service.method(sd_dbus_iface.DBUS_IFACE_FOLDERS_NAME,
3444@@ -231,9 +237,10 @@
3445 """Create a user defined folder in the specified path."""
3446 if path == '': # simulate an error
3447 self.emit_folder_create_error(path, 'create failed!')
3448+ return
3449
3450 udf = self._new_udf(self.udf_id, path)
3451- self.udfs[self.udf_id] = udf
3452+ self.udfs[udf['volume_id']] = udf
3453 self.udf_id += 1
3454 self.emit_folder_created(udf)
3455
3456@@ -243,6 +250,7 @@
3457 """Delete the folder specified by folder_id"""
3458 if folder_id not in self.udfs:
3459 self.emit_folder_delete_error(folder_id, 'failed!')
3460+ return
3461
3462 udf = self.udfs.pop(folder_id)
3463 self.emit_folder_deleted(udf)
3464@@ -260,8 +268,9 @@
3465 if folder_id not in self.udfs:
3466 # simulate error
3467 self.emit_folder_subscribe_error(folder_id, 'some error')
3468+ return
3469
3470- self.udfs[folder_id]['subscribed'] = True
3471+ self.udfs[folder_id]['subscribed'] = sd_dbus_iface.bool_str(True)
3472 self.emit_folder_subscribed(self.udfs[folder_id])
3473
3474 @dbus.service.method(sd_dbus_iface.DBUS_IFACE_FOLDERS_NAME,
3475@@ -271,8 +280,9 @@
3476 if folder_id not in self.udfs:
3477 # simulate error
3478 self.emit_folder_unsubscribe_error(folder_id, 'some error')
3479+ return
3480
3481- self.udfs[folder_id]['subscribed'] = False
3482+ self.udfs[folder_id]['subscribed'] = sd_dbus_iface.bool_str(False)
3483 self.emit_folder_unsubscribed(self.udfs[folder_id])
3484
3485 @dbus.service.method(sd_dbus_iface.DBUS_IFACE_FOLDERS_NAME,
3486@@ -296,49 +306,208 @@
3487
3488 def setUp(self):
3489 super(FoldersTestCase, self).setUp()
3490-
3491- def _get_udf_dict(udf):
3492- """Get a dict with all the attributes of: udf."""
3493- udf_dict = udf.copy()
3494- for k, val in udf_dict.items():
3495- if val is None:
3496- udf_dict[unicode(k)] = ''
3497- elif k == 'subscribed':
3498- udf_dict[unicode(k)] = sd_dbus_iface.bool_str(val)
3499- elif k in ('path', 'suggested_path') and isinstance(val, str):
3500- udf_dict[unicode(k)] = val.decode('utf-8')
3501- else:
3502- udf_dict[unicode(k)] = unicode(val)
3503- return udf_dict
3504-
3505- self.patch(sd_dbus_iface, '_get_udf_dict', _get_udf_dict)
3506+ self.patch(sd_dbus_iface, '_get_udf_dict', lambda udf: udf)
3507 self.register_mockserver(sd_dbus_iface.DBUS_IFACE_NAME,
3508 "/folders", FoldersMockDBusSyncDaemon)
3509
3510 @inlineCallbacks
3511- def test_get_volumes(self):
3512- """Retrieve volumes info list."""
3513+ def test_get_folders(self):
3514+ """Retrieve folders info list."""
3515 path = '~/bar/baz'
3516- yield dbus_client.create_volume(path)
3517+ yield dbus_client.create_folder(path)
3518
3519- result = yield dbus_client.get_volumes()
3520+ result = yield dbus_client.get_folders()
3521
3522 expected = FoldersMockDBusSyncDaemon._new_udf(1, path)
3523 self.assertEqual(result, [sd_dbus_iface._get_udf_dict(expected)])
3524
3525 @inlineCallbacks
3526- def test_create_volume(self):
3527- """Create a new volume."""
3528- path = '~/bar/baz'
3529- volume_info = yield dbus_client.create_volume(path)
3530+ def test_get_folders_error(self):
3531+ """Handle error when retrieving current syncdaemon status."""
3532+ path = '~/bar/baz'
3533+ yield dbus_client.create_folder(path)
3534+
3535+ def fail(value):
3536+ """Fake an error."""
3537+ raise TestDBusException(value)
3538+
3539+ self.patch(sd_dbus_iface, '_get_udf_dict', fail)
3540+
3541+ try:
3542+ yield dbus_client.get_folders()
3543+ except dbus.DBusException:
3544+ pass # test passes!
3545+ else:
3546+ self.fail('dbus_client.get_folders should be errbacking')
3547+
3548+ @inlineCallbacks
3549+ def test_create_folder(self):
3550+ """Create a new folder."""
3551+ path = '~/bar/baz'
3552+ folder_info = yield dbus_client.create_folder(path)
3553
3554 expected = FoldersMockDBusSyncDaemon._new_udf(1, path)
3555- self.assertEqual(sd_dbus_iface._get_udf_dict(expected), volume_info)
3556-
3557- @inlineCallbacks
3558- def test_create_volume_error(self):
3559- """Create a new volume fails."""
3560- try:
3561- yield dbus_client.create_volume(path='')
3562- except dbus_client.VolumesError, e:
3563- self.assertEqual(e[0], {'path': ''})
3564+ self.assertEqual(sd_dbus_iface._get_udf_dict(expected), folder_info)
3565+
3566+ @inlineCallbacks
3567+ def test_create_folder_error(self):
3568+ """Create a new folder fails."""
3569+ path = ''
3570+ try:
3571+ yield dbus_client.create_folder(path=path)
3572+ except dbus_client.VolumesError, e:
3573+ self.assertEqual(e[0], {'path': path})
3574+ else:
3575+ self.fail('dbus_client.create_folder should be errbacking')
3576+
3577+ @inlineCallbacks
3578+ def test_subscribe_folder(self):
3579+ """Subscribe to a folder."""
3580+ path = '~/bar/baz'
3581+ folder_info = yield dbus_client.create_folder(path)
3582+ fid = folder_info['volume_id']
3583+ yield dbus_client.subscribe_folder(fid)
3584+
3585+ result = yield dbus_client.get_folders()
3586+ expected, = filter(lambda folder: folder['volume_id'] == fid, result)
3587+ self.assertEqual(expected['subscribed'], 'True')
3588+
3589+ @inlineCallbacks
3590+ def test_subscribe_folder_error(self):
3591+ """Subscribe to a folder."""
3592+ fid = u'does not exist'
3593+ try:
3594+ yield dbus_client.subscribe_folder(fid)
3595+ except dbus_client.VolumesError, e:
3596+ self.assertEqual(e[0], {'id': fid})
3597+ else:
3598+ self.fail('dbus_client.subscribe_folder should be errbacking')
3599+
3600+ @inlineCallbacks
3601+ def test_unsubscribe_folder(self):
3602+ """Unsubscribe to a folder."""
3603+ path = '~/bar/baz'
3604+ folder_info = yield dbus_client.create_folder(path)
3605+ fid = folder_info['volume_id']
3606+ yield dbus_client.subscribe_folder(fid)
3607+ # folder is subscribed
3608+
3609+ yield dbus_client.unsubscribe_folder(fid)
3610+
3611+ result = yield dbus_client.get_folders()
3612+ expected, = filter(lambda folder: folder['volume_id'] == fid, result)
3613+ self.assertEqual(expected['subscribed'], '')
3614+
3615+ @inlineCallbacks
3616+ def test_unsubscribe_folder_error(self):
3617+ """Unsubscribe to a folder."""
3618+ fid = u'does not exist'
3619+ try:
3620+ yield dbus_client.unsubscribe_folder(fid)
3621+ except dbus_client.VolumesError, e:
3622+ self.assertEqual(e[0], {'id': fid})
3623+ else:
3624+ self.fail('dbus_client.unsubscribe_folder should be errbacking')
3625+
3626+
3627+class StatusMockDBusSyncDaemon(dbus.service.Object):
3628+ """A mock object that mimicks syncdaemon regarding the Status iface."""
3629+
3630+ state_dict = {
3631+ 'name': 'TEST',
3632+ 'description': 'Some test state, nothing else.',
3633+ 'is_error': '',
3634+ 'is_connected': 'True',
3635+ 'is_online': '',
3636+ 'queues': 'GORGEOUS',
3637+ 'connection': '',
3638+ }
3639+
3640+ def _get_current_state(self):
3641+ """Get the current status of the system."""
3642+ return self.state_dict
3643+
3644+ @dbus.service.method(sd_dbus_iface.DBUS_IFACE_STATUS_NAME,
3645+ in_signature='', out_signature='a{ss}')
3646+ def current_status(self):
3647+ """Return the current faked status of the system."""
3648+ return self._get_current_state()
3649+
3650+ # pylint: disable=C0103
3651+ # Invalid name "StatusChanged"
3652+
3653+ @dbus.service.signal(sd_dbus_iface.DBUS_IFACE_STATUS_NAME)
3654+ def StatusChanged(self, status):
3655+ """Fire a signal to notify that the status of the system changed."""
3656+
3657+ def emit_status_changed(self, state=None):
3658+ """Emit StatusChanged."""
3659+ self.StatusChanged(self._get_current_state())
3660+
3661+
3662+class StatusTestCase(DBusClientTestCase):
3663+ """Test for the status dbus client methods."""
3664+
3665+ def setUp(self):
3666+ super(StatusTestCase, self).setUp()
3667+ self.register_mockserver(sd_dbus_iface.DBUS_IFACE_NAME,
3668+ "/status", StatusMockDBusSyncDaemon)
3669+
3670+ @inlineCallbacks
3671+ def test_get_current_status(self):
3672+ """Retrieve current syncdaemon status."""
3673+ status = yield dbus_client.get_current_status()
3674+
3675+ self.assertEqual(StatusMockDBusSyncDaemon.state_dict, status)
3676+
3677+ @inlineCallbacks
3678+ def test_get_current_status_error(self):
3679+ """Handle error when retrieving current syncdaemon status."""
3680+
3681+ def fail(value):
3682+ """Fake an error."""
3683+ raise TestDBusException(value)
3684+
3685+ self.patch(StatusMockDBusSyncDaemon, '_get_current_state', fail)
3686+
3687+ try:
3688+ yield dbus_client.get_current_status()
3689+ except dbus.DBusException:
3690+ pass # test passes!
3691+ else:
3692+ self.fail('dbus_client.get_current_status should be errbacking')
3693+
3694+ def test_set_status_changed_handler(self):
3695+ """A proper callback can be connected to StatusChanged signal."""
3696+ _, sig = dbus_client.set_status_changed_handler(self._set_called)
3697+
3698+ self.assertEqual(sig._handler, self._set_called)
3699+ self.assertEqual(sig._member, 'StatusChanged')
3700+ self.assertEqual(sig._path, '/status')
3701+ self.assertEqual(sig._interface, sd_dbus_iface.DBUS_IFACE_STATUS_NAME)
3702+
3703+
3704+class FileSyncTestCase(DBusClientTestCase):
3705+ """Test for the files sync enabled dbus client methods."""
3706+
3707+ @inlineCallbacks
3708+ def test_files_sync_enabled(self):
3709+ """Retrieve whether file sync is enabled."""
3710+ expected = object()
3711+ self.patch(dbus_client.SyncDaemonTool, 'is_files_sync_enabled',
3712+ lambda _: expected)
3713+
3714+ enabled = yield dbus_client.files_sync_enabled()
3715+
3716+ self.assertEqual(expected, enabled)
3717+
3718+ @inlineCallbacks
3719+ def test_set_files_sync_enabled(self):
3720+ """Set if file sync is enabled or not."""
3721+ self.patch(dbus_client.SyncDaemonTool, 'enable_files_sync',
3722+ self._set_called)
3723+ expected = object()
3724+ # set the opposite value
3725+ yield dbus_client.set_files_sync_enabled(expected)
3726+
3727+ self.assertEqual(self._called, ((expected,), {}))
3728
3729=== modified file 'ubuntuone/controlpanel/integrationtests/test_dbus_client_sso.py'
3730--- ubuntuone/controlpanel/integrationtests/test_dbus_client_sso.py 2010-12-06 12:27:11 +0000
3731+++ ubuntuone/controlpanel/integrationtests/test_dbus_client_sso.py 2010-12-22 14:37:52 +0000
3732@@ -23,8 +23,9 @@
3733 # DBus signals have CamelCased names
3734
3735 import dbus
3736-import ubuntu_sso
3737
3738+from ubuntu_sso import (DBUS_BUS_NAME,
3739+ DBUS_CREDENTIALS_PATH, DBUS_CREDENTIALS_IFACE)
3740 from twisted.internet.defer import inlineCallbacks
3741
3742 from ubuntuone.controlpanel import dbus_client
3743@@ -36,6 +37,8 @@
3744 "token": "ABCDEF12345678",
3745 "access_token": "DEADCAFE2010",
3746 }
3747+OTHER_CREDS = {"token": "other!"}
3748+SAMPLE_ERROR = {"error message": "test", "detailed_error": "error details"}
3749
3750 # pylint: disable=C0322
3751 # pylint, you have to go to decorator's school
3752@@ -44,116 +47,161 @@
3753 class MockDBusSSOService(dbus.service.Object):
3754 """A mock object that mimicks ussoc."""
3755
3756- @dbus.service.method(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3757- in_signature="sssx")
3758- def login_or_register_to_get_credentials(self, app_name, tcurl, hlp, wid):
3759- """Get creds from the keyring, login/register if needed."""
3760- self.CredentialsFound(app_name, SAMPLE_CREDS)
3761-
3762- @dbus.service.signal(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3763- signature="sa{ss}")
3764- def CredentialsFound(self, app_name, credentials):
3765- """Credentials were finally found."""
3766-
3767-
3768-class MockDBusSSOServiceOther(dbus.service.Object):
3769- """A mock object that mimicks ussoc."""
3770-
3771- @dbus.service.method(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3772- in_signature="sssx")
3773- def login_or_register_to_get_credentials(self, app_name, tcurl, hlp, wid):
3774- """Get creds from the keyring, login/register if needed."""
3775- self.CredentialsFound("wrong app name", {})
3776- self.CredentialsFound(app_name, SAMPLE_CREDS)
3777-
3778- @dbus.service.signal(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3779- signature="sa{ss}")
3780- def CredentialsFound(self, app_name, credentials):
3781- """Credentials were finally found."""
3782-
3783-
3784-class MockDBusSSOClientOtherFailing(dbus.service.Object):
3785- """A mock object that mimicks ussoc."""
3786-
3787- @dbus.service.method(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3788- in_signature="sssx")
3789- def login_or_register_to_get_credentials(self, app_name, tcurl, hlp, wid):
3790- """Get creds from the keyring, login/register if needed."""
3791- self.CredentialsError("wrong app", "error message", "error details")
3792- self.CredentialsFound(app_name, SAMPLE_CREDS)
3793-
3794- @dbus.service.signal(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3795- signature="sa{ss}")
3796- def CredentialsFound(self, app_name, credentials):
3797- """Credentials were finally found."""
3798-
3799- @dbus.service.signal(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3800- signature="sss")
3801- def CredentialsError(self, app_name, error_message, detailed_error):
3802- """Some error happened and credentials were not found."""
3803-
3804-
3805-class MockDBusSSOClientFailing(dbus.service.Object):
3806- """A mock object that mimicks ussoc but fails."""
3807-
3808- @dbus.service.method(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3809- in_signature="sssx")
3810- def login_or_register_to_get_credentials(self, app_name, tcurl, hlp, wid):
3811- """Fail while trying to get creds from the keyring."""
3812- self.CredentialsError(app_name, "error message", "error details")
3813-
3814- @dbus.service.signal(dbus_interface=ubuntu_sso.DBUS_IFACE_CRED_NAME,
3815- signature="sss")
3816- def CredentialsError(self, app_name, error_message, detailed_error):
3817- """Some error happened and credentials were not found."""
3818+ found = True
3819+ wrong_app = None
3820+ error = None
3821+
3822+ @dbus.service.method(dbus_interface=DBUS_CREDENTIALS_IFACE,
3823+ in_signature='sa{ss}', out_signature='')
3824+ def find_credentials(self, app_name, args):
3825+ """Get creds from the keyring, login/register if needed."""
3826+ if self.wrong_app is None and self.error is None:
3827+ if self.found:
3828+ self.CredentialsFound(app_name, SAMPLE_CREDS)
3829+ else:
3830+ self.CredentialsNotFound(app_name)
3831+ elif self.wrong_app is not None and self.error is None:
3832+ self.CredentialsFound(self.wrong_app, OTHER_CREDS)
3833+ elif self.wrong_app is None and self.error is not None:
3834+ self.CredentialsError(app_name, self.error)
3835+ else:
3836+ self.CredentialsError(self.wrong_app, self.error)
3837+
3838+ self.CredentialsFound(app_name, SAMPLE_CREDS)
3839+
3840+ @dbus.service.method(dbus_interface=DBUS_CREDENTIALS_IFACE,
3841+ in_signature='sa{ss}', out_signature='')
3842+ def clear_credentials(self, app_name, args):
3843+ """Clear the credentials for an application."""
3844+ self.found = False
3845+
3846+ if self.wrong_app is None and self.error is None:
3847+ self.CredentialsCleared(app_name)
3848+ elif self.wrong_app is not None and self.error is None:
3849+ self.CredentialsCleared(self.wrong_app)
3850+ elif self.wrong_app is None and self.error is not None:
3851+ self.CredentialsError(app_name, self.error)
3852+ else:
3853+ self.CredentialsError(self.wrong_app, self.error)
3854+
3855+ self.CredentialsCleared(app_name)
3856+
3857+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='sa{ss}')
3858+ def CredentialsFound(self, app_name, credentials):
3859+ """Signal thrown when the credentials are found."""
3860+
3861+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='s')
3862+ def CredentialsNotFound(self, app_name):
3863+ """Signal thrown when the credentials are not found."""
3864+
3865+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='s')
3866+ def CredentialsCleared(self, app_name):
3867+ """Signal thrown when the credentials were cleared."""
3868+
3869+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='s')
3870+ def CredentialsStored(self, app_name):
3871+ """Signal thrown when the credentials were cleared."""
3872+
3873+ @dbus.service.signal(DBUS_CREDENTIALS_IFACE, signature='sa{ss}')
3874+ def CredentialsError(self, app_name, error_dict):
3875+ """Signal thrown when there is a problem getting the credentials."""
3876
3877
3878 class SSOClientTestCase(DBusClientTestCase):
3879 """Test for the SSO dbus client."""
3880
3881+ def setUp(self):
3882+ super(SSOClientTestCase, self).setUp()
3883+ self.register_mockserver(DBUS_BUS_NAME, DBUS_CREDENTIALS_PATH,
3884+ MockDBusSSOService)
3885+ MockDBusSSOService.wrong_app = None
3886+ MockDBusSSOService.error = None
3887+ MockDBusSSOService.found = True
3888+
3889+ # get_credentials
3890+
3891 @inlineCallbacks
3892 def test_get_credentials_ok(self):
3893 """Test the success case for get_credentials."""
3894- self.register_mockserver(ubuntu_sso.DBUS_BUS_NAME,
3895- ubuntu_sso.DBUS_CRED_PATH, MockDBusSSOService)
3896 creds = yield dbus_client.get_credentials()
3897 self.assertEqual(creds, SAMPLE_CREDS)
3898
3899 @inlineCallbacks
3900+ def test_get_credentials_not_found(self):
3901+ """Credentials were not found."""
3902+ yield dbus_client.clear_credentials() # not found will be sent
3903+ yield self.assertFailure(dbus_client.get_credentials(),
3904+ dbus_client.CredentialsError)
3905+
3906+ @inlineCallbacks
3907 def test_get_credentials_other(self):
3908 """Creds for other apps are ignored."""
3909- self.register_mockserver(ubuntu_sso.DBUS_BUS_NAME,
3910- ubuntu_sso.DBUS_CRED_PATH,
3911- MockDBusSSOServiceOther)
3912+ MockDBusSSOService.wrong_app = 'other app!'
3913 creds = yield dbus_client.get_credentials()
3914 self.assertEqual(creds, SAMPLE_CREDS)
3915
3916 @inlineCallbacks
3917 def test_get_credentials_error(self):
3918 """Test what happens when the creds can't be retrieved."""
3919- self.register_mockserver(ubuntu_sso.DBUS_BUS_NAME,
3920- ubuntu_sso.DBUS_CRED_PATH,
3921- MockDBusSSOClientFailing)
3922-
3923+ MockDBusSSOService.error = SAMPLE_ERROR
3924 yield self.assertFailure(dbus_client.get_credentials(),
3925 dbus_client.CredentialsError)
3926
3927 @inlineCallbacks
3928 def test_get_credentials_other_error(self):
3929 """Other creds err before ours are retrieved."""
3930- self.register_mockserver(ubuntu_sso.DBUS_BUS_NAME,
3931- ubuntu_sso.DBUS_CRED_PATH,
3932- MockDBusSSOClientOtherFailing)
3933-
3934+ MockDBusSSOService.wrong_app = 'other app!'
3935+ MockDBusSSOService.error = SAMPLE_ERROR
3936 creds = yield dbus_client.get_credentials()
3937 self.assertEqual(creds, SAMPLE_CREDS)
3938
3939+ # clear_credentials
3940+
3941+ @inlineCallbacks
3942+ def test_clear_credentials_ok(self):
3943+ """Test the success case for clear_credentials."""
3944+ result = yield dbus_client.clear_credentials()
3945+ self.assertEqual(result, dbus_client.APP_NAME)
3946+
3947+ @inlineCallbacks
3948+ def test_clear_credentials_other(self):
3949+ """Creds for other apps are ignored."""
3950+ MockDBusSSOService.wrong_app = 'other app!'
3951+ result = yield dbus_client.clear_credentials()
3952+ self.assertEqual(result, dbus_client.APP_NAME)
3953+
3954+ @inlineCallbacks
3955+ def test_clear_credentials_error(self):
3956+ """Test what happens when the creds can't be retrieved."""
3957+ MockDBusSSOService.error = SAMPLE_ERROR
3958+ yield self.assertFailure(dbus_client.clear_credentials(),
3959+ dbus_client.CredentialsError)
3960+
3961+ @inlineCallbacks
3962+ def test_clear_credentials_other_error(self):
3963+ """Other creds err before ours are retrieved."""
3964+ MockDBusSSOService.wrong_app = 'other app!'
3965+ MockDBusSSOService.error = SAMPLE_ERROR
3966+ result = yield dbus_client.clear_credentials()
3967+ self.assertEqual(result, dbus_client.APP_NAME)
3968+
3969+
3970+class NoMethodsSSOClientTestCase(DBusClientTestCase):
3971+ """Test for the SSO dbus client when the service provides no methods."""
3972+
3973+ def setUp(self):
3974+ super(NoMethodsSSOClientTestCase, self).setUp()
3975+ self.register_mockserver(DBUS_BUS_NAME, DBUS_CREDENTIALS_PATH,
3976+ MockDBusNoMethods)
3977+
3978 @inlineCallbacks
3979 def test_get_credentials_dbus_error(self):
3980 """Test what happens when there's a DBus error."""
3981- self.register_mockserver(ubuntu_sso.DBUS_BUS_NAME,
3982- ubuntu_sso.DBUS_CRED_PATH,
3983- MockDBusNoMethods)
3984+ yield self.assertFailure(dbus_client.get_credentials(),
3985+ dbus.DBusException)
3986
3987- yield self.assertFailure(dbus_client.get_credentials(),
3988+ @inlineCallbacks
3989+ def test_clear_credentials_dbus_error(self):
3990+ """Test what happens when there's a DBus error."""
3991+ yield self.assertFailure(dbus_client.clear_credentials(),
3992 dbus.DBusException)
3993
3994=== modified file 'ubuntuone/controlpanel/integrationtests/test_dbus_service.py'
3995--- ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2010-12-06 12:27:11 +0000
3996+++ ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2010-12-22 14:37:52 +0000
3997@@ -45,8 +45,8 @@
3998 "name": "Ubuntu One @ darkstar",
3999 "date_added": "2008-12-06T18:15:38.0",
4000 "type": "computer",
4001- "configurable": "1",
4002- "limit_bandwidth": "1",
4003+ "configurable": 'True',
4004+ "limit_bandwidth": 'True',
4005 "max_upload_speed": "12345",
4006 "max_download_speed": "54321",
4007 "available_services": "files, contacts, music, bookmarks",
4008@@ -57,7 +57,7 @@
4009 "name": "Ubuntu One @ brightmoon",
4010 "date_added": "2010-09-22T20:45:38.0",
4011 "type": "computer",
4012- "configurable": "0",
4013+ "configurable": '',
4014 "available_services": "files, contacts, bookmarks",
4015 "enabled_services": "files, bookmarks",
4016 },
4017@@ -68,19 +68,19 @@
4018 "volume_id": "volume-0",
4019 "path": "/home/user/Documents",
4020 "suggested_path": "~/Documents",
4021- "subscribed": "1",
4022+ "subscribed": 'True',
4023 },
4024 {
4025 "volume_id": "volume-1",
4026 "path": "/home/user/Music",
4027 "suggested_path": "~/Music",
4028- "subscribed": "0",
4029+ "subscribed": '',
4030 },
4031 {
4032 "volume_id": "volume-2",
4033 "path": "/home/user/Pictures/Photos",
4034 "suggested_path": "~/Pictures/Photos",
4035- "subscribed": "0",
4036+ "subscribed": '',
4037 },
4038 ]
4039
4040@@ -115,7 +115,13 @@
4041
4042 class MockBackend(object):
4043 """A mock backend."""
4044+
4045 exception = None
4046+ sample_status = {
4047+ dbus_service.MSG_KEY: 'test me please',
4048+ dbus_service.STATUS_KEY: dbus_service.FILE_SYNC_IDLE,
4049+ }
4050+ status_changed_handler = None
4051
4052 def _process(self, result):
4053 """Process the request with the given result."""
4054@@ -142,7 +148,7 @@
4055
4056 def file_sync_status(self):
4057 """Return the status of the file sync service."""
4058- return self._process((True, "Synchronizing"))
4059+ return self._process(self.sample_status)
4060
4061 def volumes_info(self):
4062 """Get the user volumes info."""
4063@@ -170,6 +176,11 @@
4064 """Initialize each test run."""
4065 super(DBusServiceTestCase, self).setUp()
4066 dbus_service.init_mainloop()
4067+ self._called = False
4068+
4069+ def _set_called(self, *args, **kwargs):
4070+ """Keep track of function calls, useful for monkeypatching."""
4071+ self._called = (args, kwargs)
4072
4073 def test_register_service(self):
4074 """The DBus service is successfully registered."""
4075@@ -193,7 +204,7 @@
4076 def test_error_handler_with_failure(self):
4077 """Ensure to build a string-string dict to pass to error signals."""
4078 error = dbus_service.Failure(TypeError('oh no!'))
4079- expected = dbus_service.utils.failure_to_error_dict(error)
4080+ expected = dbus_service.failure_to_error_dict(error)
4081
4082 result = dbus_service.error_handler(error)
4083
4084@@ -202,7 +213,7 @@
4085 def test_error_handler_with_exception(self):
4086 """Ensure to build a string-string dict to pass to error signals."""
4087 error = TypeError('oh no, no again!')
4088- expected = dbus_service.utils.exception_to_error_dict(error)
4089+ expected = dbus_service.exception_to_error_dict(error)
4090
4091 result = dbus_service.error_handler(error)
4092
4093@@ -231,8 +242,8 @@
4094 def test_error_handler_default(self):
4095 """Ensure to build a string-string dict to pass to error signals."""
4096 msg = 'Got unexpected error argument %r' % None
4097- expected = {dbus_service.utils.ERROR_TYPE: 'UnknownError',
4098- dbus_service.utils.ERROR_MESSAGE: msg}
4099+ expected = {dbus_service.ERROR_TYPE: 'UnknownError',
4100+ dbus_service.ERROR_MESSAGE: msg}
4101
4102 result = dbus_service.error_handler(None)
4103
4104@@ -330,7 +341,7 @@
4105 self.assertIn("date_added", device_info)
4106 self.assertIn("type", device_info)
4107 self.assertIn("configurable", device_info)
4108- if int(device_info["configurable"]):
4109+ if bool(device_info["configurable"]):
4110 self.assertIn("limit_bandwidth", device_info)
4111 self.assertIn("max_upload_speed", device_info)
4112 self.assertIn("max_download_speed", device_info)
4113@@ -372,19 +383,6 @@
4114 self.backend.remove_device, sample_token)
4115 return self.assert_correct_method_call(*args)
4116
4117- def test_file_sync_status(self):
4118- """The file sync status is reported."""
4119-
4120- def got_signal(enabled, status):
4121- """The correct status was received."""
4122- self.assertEqual(enabled, True)
4123- self.assertEqual(status, "Synchronizing")
4124- self.deferred.callback("success")
4125-
4126- args = ("FileSyncStatusReady", "FileSyncStatusError", got_signal,
4127- self.backend.file_sync_status)
4128- return self.assert_correct_method_call(*args)
4129-
4130 def test_volumes_info(self):
4131 """The volumes info is reported."""
4132
4133@@ -408,7 +406,7 @@
4134
4135 args = ("VolumeSettingsChanged", "VolumeSettingsChangeError",
4136 got_signal, self.backend.change_volume_settings,
4137- expected_volume_id, {'subscribed': '0'})
4138+ expected_volume_id, {'subscribed': ''})
4139 return self.assert_correct_method_call(*args)
4140
4141 def test_query_bookmarks_extension(self):
4142@@ -451,11 +449,87 @@
4143
4144 """
4145
4146- def got_error_signal(error_dict):
4147+ def got_error_signal(*a):
4148 """The error signal was received."""
4149- self.assertEqual(error_dict[dbus_service.utils.ERROR_TYPE],
4150+ if len(a) == 1:
4151+ error_dict = a[0]
4152+ else:
4153+ an_id, error_dict = a
4154+ self.assertEqual(an_id, args[0])
4155+
4156+ self.assertEqual(error_dict[dbus_service.ERROR_TYPE],
4157 'AssertionError')
4158 self.deferred.callback("success")
4159
4160 return super(OperationsErrorTestCase, self).assert_correct_method_call(
4161 error_sig, success_sig, got_error_signal, method, *args)
4162+
4163+
4164+class FileSyncTestCase(OperationsTestCase):
4165+ """Test for the DBus service when requesting file sync status."""
4166+
4167+ def assert_correct_status_signal(self, status, sync_signal,
4168+ error_signal="FileSyncStatusError",
4169+ expected_msg=None):
4170+ """The file sync status is reported properly."""
4171+ MockBackend.sample_status[dbus_service.STATUS_KEY] = status
4172+ if expected_msg is None:
4173+ expected_msg = MockBackend.sample_status[dbus_service.MSG_KEY]
4174+
4175+ def got_signal(msg):
4176+ """The correct status was received."""
4177+ self.assertEqual(msg, expected_msg)
4178+ self.deferred.callback("success")
4179+
4180+ args = (sync_signal, error_signal, got_signal,
4181+ self.backend.file_sync_status)
4182+ return self.assert_correct_method_call(*args)
4183+
4184+ def test_file_sync_status_unknown(self):
4185+ """The file sync status is reported properly."""
4186+ msg = MockBackend.sample_status
4187+ args = ('invalid-file-sync-status', "FileSyncStatusError",
4188+ "FileSyncStatusIdle", msg)
4189+ return self.assert_correct_status_signal(*args)
4190+
4191+ def test_file_sync_status_error(self):
4192+ """The file sync status is reported properly."""
4193+ msg = MockBackend.sample_status[dbus_service.MSG_KEY]
4194+ err_dict = {dbus_service.ERROR_TYPE: 'FileSyncStatusError',
4195+ dbus_service.ERROR_MESSAGE: msg}
4196+ args = (dbus_service.FILE_SYNC_ERROR, "FileSyncStatusError",
4197+ "FileSyncStatusIdle", err_dict)
4198+ return self.assert_correct_status_signal(*args)
4199+
4200+ def test_file_sync_status_disabled(self):
4201+ """The file sync status is reported properly."""
4202+ args = (dbus_service.FILE_SYNC_DISABLED, "FileSyncStatusDisabled")
4203+ return self.assert_correct_status_signal(*args)
4204+
4205+ def test_file_sync_status_starting(self):
4206+ """The file sync status is reported properly."""
4207+ args = (dbus_service.FILE_SYNC_STARTING, "FileSyncStatusStarting")
4208+ return self.assert_correct_status_signal(*args)
4209+
4210+ def test_file_sync_status_disconnected(self):
4211+ """The file sync status is reported properly."""
4212+ args = (dbus_service.FILE_SYNC_DISCONNECTED,
4213+ "FileSyncStatusDisconnected")
4214+ return self.assert_correct_status_signal(*args)
4215+
4216+ def test_file_sync_status_syncing(self):
4217+ """The file sync status is reported properly."""
4218+ args = (dbus_service.FILE_SYNC_SYNCING, "FileSyncStatusSyncing")
4219+ return self.assert_correct_status_signal(*args)
4220+
4221+ def test_file_sync_status_idle(self):
4222+ """The file sync status is reported properly."""
4223+ args = (dbus_service.FILE_SYNC_IDLE, "FileSyncStatusIdle")
4224+ return self.assert_correct_status_signal(*args)
4225+
4226+ def test_status_changed_handler(self):
4227+ """The status changed handler is properly set."""
4228+ be = MockBackend()
4229+ cpbe = dbus_service.ControlPanelBackend(backend=be)
4230+
4231+ self.assertEqual(be.status_changed_handler, cpbe.process_status)
4232
4233=== modified file 'ubuntuone/controlpanel/logger.py'
4234--- ubuntuone/controlpanel/logger.py 2010-12-06 12:27:11 +0000
4235+++ ubuntuone/controlpanel/logger.py 2010-12-22 14:37:52 +0000
4236@@ -22,6 +22,7 @@
4237 import os
4238 import sys
4239
4240+from functools import wraps
4241 from logging.handlers import RotatingFileHandler
4242
4243 # pylint: disable=F0401,E0611
4244@@ -41,7 +42,7 @@
4245 MAIN_HANDLER.setLevel(LOG_LEVEL)
4246
4247
4248-def setup_logging(log_domain):
4249+def setup_logging(log_domain, prefix=None):
4250 """Create a logger for 'log_domain'.
4251
4252 Final domain will be 'ubuntuone.controlpanel.<log_domain>.
4253@@ -52,6 +53,28 @@
4254 logger.addHandler(MAIN_HANDLER)
4255 if os.environ.get('DEBUG'):
4256 debug_handler = logging.StreamHandler(sys.stderr)
4257+ if prefix is not None:
4258+ fmt = prefix + "%(name)s - %(levelname)s\n%(message)s\n"
4259+ formatter = logging.Formatter(fmt)
4260+ debug_handler.setFormatter(formatter)
4261 logger.addHandler(debug_handler)
4262
4263 return logger
4264+
4265+
4266+def log_call(log_func):
4267+ """Decorator to add log info using 'log_func'."""
4268+
4269+ def middle(f):
4270+ """Add logging when calling 'f'."""
4271+
4272+ @wraps(f)
4273+ def inner(*args, **kwargs):
4274+ """Call f(*args, **kwargs)."""
4275+ log_func('%s: args %r, kwargs %r.', f.__name__, args, kwargs)
4276+ res = f(*args, **kwargs)
4277+ return res
4278+
4279+ return inner
4280+
4281+ return middle
4282
4283=== modified file 'ubuntuone/controlpanel/tests/test_backend.py'
4284--- ubuntuone/controlpanel/tests/test_backend.py 2010-12-06 12:27:11 +0000
4285+++ ubuntuone/controlpanel/tests/test_backend.py 2010-12-22 14:37:52 +0000
4286@@ -23,10 +23,21 @@
4287
4288 from twisted.internet import defer
4289 from twisted.internet.defer import inlineCallbacks
4290+from ubuntuone.devtools.handlers import MementoHandler
4291
4292 from ubuntuone.controlpanel import backend
4293-from ubuntuone.controlpanel.backend import (ACCOUNT_API, QUOTA_API,
4294- DEVICES_API, DEVICE_REMOVE_API)
4295+from ubuntuone.controlpanel.backend import (ACCOUNT_API,
4296+ DEVICES_API, DEVICE_REMOVE_API, QUOTA_API,
4297+ FILE_SYNC_DISABLED,
4298+ FILE_SYNC_DISCONNECTED,
4299+ FILE_SYNC_ERROR,
4300+ FILE_SYNC_IDLE,
4301+ FILE_SYNC_STARTING,
4302+ FILE_SYNC_SYNCING,
4303+ FILE_SYNC_UNKNOWN,
4304+ MSG_KEY, STATUS_KEY,
4305+)
4306+
4307 from ubuntuone.controlpanel.tests import TestCase
4308 from ubuntuone.controlpanel.webclient import WebClientError
4309
4310@@ -44,7 +55,7 @@
4311 "dbpath": "u/abc/def/12345"
4312 },
4313 "couchdb_root": "https://couchdb.one.ubuntu.com/u/abc/def/12345",
4314- "email": "andrewpz@protocultura.net",
4315+ "email": "andrewpz@protocultura.net",%s
4316 "nickname": "Andrew P. Zoilo",
4317 "id": 12345,
4318 "subscription": {
4319@@ -63,6 +74,13 @@
4320 }
4321 """
4322
4323+CURRENT_PLAN = "Ubuntu One Basic (2 GB) + 1 x 20-Pack with 20 GB (monthly)"
4324+SAMPLE_CURRENT_PLAN = '\n "current_plan": "%s",' % CURRENT_PLAN
4325+
4326+SAMPLE_ACCOUNT_NO_CURRENT_PLAN = SAMPLE_ACCOUNT_JSON % ''
4327+SAMPLE_ACCOUNT_WITH_CURRENT_PLAN = SAMPLE_ACCOUNT_JSON % SAMPLE_CURRENT_PLAN
4328+
4329+
4330 SAMPLE_QUOTA_JSON = """
4331 {
4332 "total": 53687091200,
4333@@ -78,6 +96,14 @@
4334 "email": "andrewpz@protocultura.net",
4335 }
4336
4337+EXPECTED_ACCOUNT_INFO_WITH_CURRENT_PLAN = {
4338+ "quota_used": "2350345156",
4339+ "quota_total": "53687091200",
4340+ "type": CURRENT_PLAN,
4341+ "name": "Andrew P. Zoilo",
4342+ "email": "andrewpz@protocultura.net",
4343+}
4344+
4345 SAMPLE_DEVICES_JSON = """
4346 [
4347 {
4348@@ -103,12 +129,14 @@
4349 "device_id": "ComputerABCDEF01234token",
4350 "name": "Ubuntu One @ darkstar",
4351 "type": "Computer",
4352- "configurable": "0",
4353+ "is_local": '',
4354+ "configurable": '',
4355 },
4356 {
4357- 'configurable': '1',
4358+ 'is_local': 'True',
4359+ 'configurable': 'True',
4360 'device_id': 'ComputerABC1234DEF',
4361- 'limit_bandwidth': "0",
4362+ 'limit_bandwidth': '',
4363 'max_download_speed': '-1',
4364 'max_upload_speed': '-1',
4365 'name': 'Ubuntu One @ localhost',
4366@@ -118,13 +146,49 @@
4367 "device_id": "Phone1000",
4368 "name": "Nokia E65",
4369 "type": "Phone",
4370- "configurable": "0",
4371+ "configurable": '',
4372+ "is_local": '',
4373 },
4374 ]
4375
4376-SAMPLE_VOLUMES = [
4377- {'volume_id': 'test1', 'path': '~/test1', 'subscribed': False},
4378- {'volume_id': 'test2', 'path': '~/test2', 'subscribed': True},
4379+SAMPLE_FOLDERS = [
4380+ {u'generation': u'2', u'node_id': u'341da068-81d8-437a-8f75-5bb9d86455ba',
4381+ u'path': u'/home/tester/Public', u'subscribed': u'True',
4382+ u'suggested_path': u'~/Public',
4383+ u'type': u'UDF', u'volume_id': u'9ea892f8-15fa-4201-bdbf-8de99fa5f588'},
4384+ {u'generation': u'', u'node_id': u'11fbc86c-0d7a-49f5-ae83-8402caf66c6a',
4385+ u'path': u'/home/tester/Documents', u'subscribed': u'',
4386+ u'suggested_path': u'~/Documents',
4387+ u'type': u'UDF', u'volume_id': u'2db262f5-a151-4c19-969c-bb5ced753c61'},
4388+ {u'generation': u'24', u'node_id': u'9ee0e130-a7c7-4d76-a5e3-5df506221b48',
4389+ u'path': u'/home/tester/Pictures/Photos', u'subscribed': u'True',
4390+ u'suggested_path': u'~/Pictures/Photos',
4391+ u'type': u'UDF', u'volume_id': u'1deb2874-3d28-46ae-9999-d5f48de9f460'},
4392+]
4393+
4394+SAMPLE_SHARES = [
4395+ {u'accepted': u'True', u'access_level': u'View',
4396+ u'free_bytes': u'39892622746', u'generation': u'2704',
4397+ u'name': u're', u'node_id': u'c483f419-ed28-490a-825d-a8c074e2d795',
4398+ u'other_username': u'otheruser', u'other_visible_name': u'Other User',
4399+ u'path': u'/home/tester/.local/share/ubuntuone/shares/re from Other User',
4400+ u'type': u'Share', u'volume_id': u'4a1b263b-a2b3-4f66-9e66-4cd18050810d'},
4401+ {u'accepted': u'True', u'access_level': u'Modify',
4402+ u'free_bytes': u'39892622746', u'generation': u'2704',
4403+ u'name': u'do', u'node_id': u'84544ea4-aefe-4f91-9bb9-ed7b0a805baf',
4404+ u'other_username': u'otheruser', u'other_visible_name': u'Other User',
4405+ u'path': u'/home/tester/.local/share/ubuntuone/shares/do from Other User',
4406+ u'type': u'Share', u'volume_id': u'7d130dfe-98b2-4bd5-8708-9eeba9838ac0'},
4407+]
4408+
4409+SAMPLE_SHARED = [
4410+ {u'accepted': u'True', u'access_level': u'View',
4411+ u'free_bytes': u'', u'generation': u'',
4412+ u'name': u'bar', u'node_id': u'31e47530-9448-4f03-b4dc-4154fdf35225',
4413+ u'other_username': u'otheruser', u'other_visible_name': u'Other User',
4414+ u'path': u'/home/tester/Ubuntu One/bar',
4415+ u'type': u'Shared',
4416+ u'volume_id': u'79584900-517f-4dff-b2f3-20e8c1e79365'},
4417 ]
4418
4419
4420@@ -152,11 +216,24 @@
4421 creds = SAMPLE_CREDENTIALS
4422 throttling = False
4423 limits = {"download": -1, "upload": -1}
4424+ file_sync = True
4425+ status = {
4426+ 'name': 'TEST', 'queues': 'GORGEOUS', 'connection': '',
4427+ 'description': 'Some test state, nothing else.',
4428+ 'is_error': '', 'is_connected': 'True', 'is_online': '',
4429+ }
4430+ status_changed_handler = None
4431+ subscribed_folders = []
4432
4433 def get_credentials(self):
4434 """Return the mock credentials."""
4435 return defer.succeed(self.creds)
4436
4437+ def clear_credentials(self):
4438+ """Clear the mock credentials."""
4439+ MockDBusClient.creds = None
4440+ return defer.succeed(None)
4441+
4442 def get_throttling_limits(self):
4443 """Return the sample speed limits."""
4444 return self.limits
4445@@ -178,9 +255,41 @@
4446 """Disable bw throttling."""
4447 self.throttling = False
4448
4449- def get_volumes(self):
4450- """Grab list of folders and shares."""
4451- return SAMPLE_VOLUMES
4452+ def files_sync_enabled(self):
4453+ """Get if file sync service is enabled."""
4454+ return self.file_sync
4455+
4456+ def set_files_sync_enabled(self, enabled):
4457+ """Set the file sync service to be 'enabled'."""
4458+ self.file_sync = enabled
4459+
4460+ def get_folders(self):
4461+ """Grab list of folders."""
4462+ return SAMPLE_FOLDERS
4463+
4464+ def subscribe_folder(self, volume_id):
4465+ """Subcribe to 'volume_id'."""
4466+ self.subscribed_folders.append(volume_id)
4467+
4468+ def unsubscribe_folder(self, volume_id):
4469+ """Unsubcribe from 'volume_id'."""
4470+ self.subscribed_folders.remove(volume_id)
4471+
4472+ def get_current_status(self):
4473+ """Grab syncdaemon status."""
4474+ return self.status
4475+
4476+ def set_status_changed_handler(self, handler):
4477+ """Connect a handler for tracking syncdaemon status changes."""
4478+ self.status_changed_handler = handler
4479+
4480+ def get_shares(self):
4481+ """Grab list of shares (shares from others to the user)."""
4482+ return SAMPLE_SHARES
4483+
4484+ def get_shared(self):
4485+ """Grab list of shared (shares from the user to others)."""
4486+ return SAMPLE_SHARED
4487
4488
4489 class BackendBasicTestCase(TestCase):
4490@@ -189,11 +298,17 @@
4491 timeout = 3
4492
4493 def setUp(self):
4494+ super(BackendBasicTestCase, self).setUp()
4495 self.patch(backend, "WebClient", MockWebClient)
4496 self.patch(backend, "dbus_client", MockDBusClient())
4497 self.local_token = "Computer" + SAMPLE_CREDENTIALS["token"]
4498 self.be = backend.ControlBackend()
4499
4500+ self.memento = MementoHandler()
4501+ backend.logger.addHandler(self.memento)
4502+
4503+ MockDBusClient.creds = SAMPLE_CREDENTIALS
4504+
4505 def test_backend_creation(self):
4506 """The backend instance is successfully created."""
4507 self.assertEqual(self.be.wc.__class__, MockWebClient)
4508@@ -204,6 +319,16 @@
4509 token = yield self.be.get_token()
4510 self.assertEqual(token, SAMPLE_CREDENTIALS["token"])
4511
4512+ @inlineCallbacks
4513+ def test_device_is_local(self):
4514+ """The device_is_local returns the right result."""
4515+ result = yield self.be.device_is_local(self.local_token)
4516+ self.assertTrue(result)
4517+
4518+ did = EXPECTED_DEVICES_INFO[0]['device_id']
4519+ result = yield self.be.device_is_local(did)
4520+ self.assertFalse(result)
4521+
4522
4523 class BackendAccountTestCase(BackendBasicTestCase):
4524 """Account tests for the backend."""
4525@@ -212,12 +337,21 @@
4526 def test_account_info(self):
4527 """The account_info method exercises its callback."""
4528 # pylint: disable=E1101
4529- self.be.wc.results[ACCOUNT_API] = SAMPLE_ACCOUNT_JSON
4530+ self.be.wc.results[ACCOUNT_API] = SAMPLE_ACCOUNT_NO_CURRENT_PLAN
4531 self.be.wc.results[QUOTA_API] = SAMPLE_QUOTA_JSON
4532 result = yield self.be.account_info()
4533 self.assertEqual(result, EXPECTED_ACCOUNT_INFO)
4534
4535 @inlineCallbacks
4536+ def test_account_info_with_current_plan(self):
4537+ """The account_info method exercises its callback."""
4538+ # pylint: disable=E1101
4539+ self.be.wc.results[ACCOUNT_API] = SAMPLE_ACCOUNT_WITH_CURRENT_PLAN
4540+ self.be.wc.results[QUOTA_API] = SAMPLE_QUOTA_JSON
4541+ result = yield self.be.account_info()
4542+ self.assertEqual(result, EXPECTED_ACCOUNT_INFO_WITH_CURRENT_PLAN)
4543+
4544+ @inlineCallbacks
4545 def test_account_info_fails(self):
4546 """The account_info method exercises its errback."""
4547 # pylint: disable=E1101
4548@@ -248,11 +382,23 @@
4549 """The remove_device method calls the right api."""
4550 dtype, did = "Computer", "SAMPLE-TOKEN"
4551 device_id = dtype + did
4552- apiurl = DEVICE_REMOVE_API % (dtype, did)
4553+ apiurl = DEVICE_REMOVE_API % (dtype.lower(), did)
4554 # pylint: disable=E1101
4555 self.be.wc.results[apiurl] = SAMPLE_DEVICES_JSON
4556 result = yield self.be.remove_device(device_id)
4557 self.assertEqual(result, device_id)
4558+ # credentials were not cleared
4559+ self.assertEqual(MockDBusClient.creds, SAMPLE_CREDENTIALS)
4560+
4561+ @inlineCallbacks
4562+ def test_remove_device_clear_credentials_if_local_device(self):
4563+ """The remove_device method clears the credentials if is local."""
4564+ apiurl = DEVICE_REMOVE_API % ('computer', SAMPLE_CREDENTIALS['token'])
4565+ # pylint: disable=E1101
4566+ self.be.wc.results[apiurl] = SAMPLE_DEVICES_JSON
4567+ yield self.be.remove_device(self.local_token)
4568+ # credentials were cleared
4569+ self.assertEqual(MockDBusClient.creds, None)
4570
4571 @inlineCallbacks
4572 def test_remove_device_fails(self):
4573@@ -266,10 +412,10 @@
4574 """The device settings are updated."""
4575 backend.dbus_client.throttling = False
4576 yield self.be.change_device_settings(self.local_token,
4577- {"limit_bandwidth": "1"})
4578+ {"limit_bandwidth": 'True'})
4579 self.assertEqual(backend.dbus_client.throttling, True)
4580 yield self.be.change_device_settings(self.local_token,
4581- {"limit_bandwidth": "0"})
4582+ {"limit_bandwidth": ''})
4583 self.assertEqual(backend.dbus_client.throttling, False)
4584
4585 @inlineCallbacks
4586@@ -298,7 +444,7 @@
4587 new_settings = {
4588 "max_download_speed": "99",
4589 "max_upload_speed": "99",
4590- "limit_bandwidth": "1",
4591+ "limit_bandwidth": 'True',
4592 }
4593 yield self.be.change_device_settings("wrong token!", new_settings)
4594 self.assertEqual(backend.dbus_client.throttling, False)
4595@@ -313,4 +459,197 @@
4596 def test_volumes_info(self):
4597 """The volumes_info method exercises its callback."""
4598 result = yield self.be.volumes_info()
4599- self.assertEqual(result, SAMPLE_VOLUMES)
4600+ # later, we should unify folders and shares info
4601+ self.assertEqual(result, SAMPLE_FOLDERS)
4602+
4603+ @inlineCallbacks
4604+ def test_subscribe_volume(self):
4605+ """The subscribe_volume method subscribes a folder."""
4606+ fid = '0123-4567'
4607+ yield self.be.subscribe_volume(volume_id=fid)
4608+ self.addCleanup(lambda: MockDBusClient.subscribed_folders.remove(fid))
4609+
4610+ self.assertEqual(MockDBusClient.subscribed_folders, [fid])
4611+
4612+ @inlineCallbacks
4613+ def test_unsubscribe_volume(self):
4614+ """The unsubscribe_volume method unsubscribes a folder."""
4615+ fid = '0123-4567'
4616+ yield self.be.subscribe_volume(volume_id=fid)
4617+ self.assertEqual(MockDBusClient.subscribed_folders, [fid])
4618+
4619+ yield self.be.unsubscribe_volume(volume_id=fid)
4620+
4621+ self.assertEqual(MockDBusClient.subscribed_folders, [])
4622+
4623+ @inlineCallbacks
4624+ def test_change_volume_settings(self):
4625+ """The volume settings can be changed."""
4626+ fid = '0123-4567'
4627+
4628+ yield self.be.change_volume_settings(fid, {'subscribed': True})
4629+ self.assertEqual(MockDBusClient.subscribed_folders, [fid])
4630+
4631+ yield self.be.change_volume_settings(fid, {'subscribed': False})
4632+ self.assertEqual(MockDBusClient.subscribed_folders, [])
4633+
4634+ @inlineCallbacks
4635+ def test_change_volume_settings_no_setting(self):
4636+ """The change volume settings does not fail on empty settings."""
4637+ yield self.be.change_volume_settings('test', {})
4638+ self.assertEqual(MockDBusClient.subscribed_folders, [])
4639+
4640+
4641+class BackendSyncStatusTestCase(BackendBasicTestCase):
4642+ """Syncdaemon state for the backend."""
4643+
4644+ def _build_msg(self):
4645+ """Build expected message regarding file sync status."""
4646+ return '%s (%s)' % (MockDBusClient.status['description'],
4647+ MockDBusClient.status['name'])
4648+
4649+ @inlineCallbacks
4650+ def assert_correct_status(self, status, msg=None):
4651+ """Check that the resulting status is correct."""
4652+ expected = {MSG_KEY: self._build_msg() if msg is None else msg,
4653+ STATUS_KEY: status}
4654+ result = yield self.be.file_sync_status()
4655+ self.assertEqual(expected, result)
4656+
4657+ @inlineCallbacks
4658+ def test_disabled(self):
4659+ """The syncdaemon status is processed and emitted."""
4660+ self.patch(MockDBusClient, 'file_sync', False)
4661+ yield self.assert_correct_status(FILE_SYNC_DISABLED, msg='')
4662+
4663+ @inlineCallbacks
4664+ def test_error(self):
4665+ """The syncdaemon status is processed and emitted."""
4666+ MockDBusClient.status = {
4667+ 'is_error': 'True', # nothing else matters
4668+ 'is_online': '', 'is_connected': '',
4669+ 'name': 'AUTH_FAILED', 'connection': '', 'queues': '',
4670+ 'description': 'auth failed',
4671+ }
4672+ yield self.assert_correct_status(FILE_SYNC_ERROR)
4673+
4674+ @inlineCallbacks
4675+ def test_starting_when_init_not_user(self):
4676+ """The syncdaemon status is processed and emitted."""
4677+ MockDBusClient.status = {
4678+ 'is_error': '', 'is_online': '', 'is_connected': '',
4679+ 'connection': 'Not User With Network', 'queues': '',
4680+ 'name': 'INIT', 'description': 'something new',
4681+ }
4682+ yield self.assert_correct_status(FILE_SYNC_STARTING)
4683+
4684+ @inlineCallbacks
4685+ def test_starting_when_init_with_user(self):
4686+ """The syncdaemon status is processed and emitted."""
4687+ MockDBusClient.status = {
4688+ 'is_error': '', 'is_online': '', 'is_connected': '',
4689+ 'connection': 'With User With Network', 'queues': '',
4690+ 'name': 'INIT', 'description': 'something new',
4691+ }
4692+ yield self.assert_correct_status(FILE_SYNC_STARTING)
4693+
4694+ @inlineCallbacks
4695+ def test_starting_when_local_rescan_not_user(self):
4696+ """The syncdaemon status is processed and emitted."""
4697+ MockDBusClient.status = {
4698+ 'is_error': '', 'is_online': '', 'is_connected': '',
4699+ 'connection': 'Not User With Network', 'queues': '',
4700+ 'name': 'LOCAL_RESCAN', 'description': 'something new',
4701+ }
4702+ yield self.assert_correct_status(FILE_SYNC_STARTING)
4703+
4704+ @inlineCallbacks
4705+ def test_starting_when_local_rescan_with_user(self):
4706+ """The syncdaemon status is processed and emitted."""
4707+ MockDBusClient.status = {
4708+ 'is_error': '', 'is_online': '', 'is_connected': '',
4709+ 'connection': 'With User With Network', 'queues': '',
4710+ 'name': 'LOCAL_RESCAN', 'description': 'something new',
4711+ }
4712+ yield self.assert_correct_status(FILE_SYNC_STARTING)
4713+
4714+ @inlineCallbacks
4715+ def test_starting_when_ready_with_user(self):
4716+ """The syncdaemon status is processed and emitted."""
4717+ MockDBusClient.status = {
4718+ 'is_error': '', 'is_online': '', 'is_connected': '',
4719+ 'connection': 'With User With Network', 'queues': '',
4720+ 'name': 'READY', 'description': 'something nicer',
4721+ }
4722+ yield self.assert_correct_status(FILE_SYNC_STARTING)
4723+
4724+ @inlineCallbacks
4725+ def test_disconnected(self):
4726+ """The syncdaemon status is processed and emitted."""
4727+ MockDBusClient.status = {
4728+ 'is_error': '', 'is_online': '', 'is_connected': '',
4729+ 'queues': '', 'description': 'something new',
4730+ 'connection': 'Not User With Network', # user didn't connect
4731+ 'name': 'READY', # must be READY, otherwise is STARTING
4732+ }
4733+ yield self.assert_correct_status(FILE_SYNC_DISCONNECTED)
4734+
4735+ @inlineCallbacks
4736+ def test_syncing_if_online(self):
4737+ """The syncdaemon status is processed and emitted."""
4738+ MockDBusClient.status = {
4739+ 'is_error': '', 'is_online': 'True', 'is_connected': 'True',
4740+ 'name': 'QUEUE_MANAGER', 'connection': '',
4741+ 'queues': 'WORKING_ON_CONTENT', # anything but IDLE
4742+ 'description': 'something nicer',
4743+ }
4744+ yield self.assert_correct_status(FILE_SYNC_SYNCING)
4745+
4746+ @inlineCallbacks
4747+ def test_syncing_even_if_not_online(self):
4748+ """The syncdaemon status is processed and emitted."""
4749+ MockDBusClient.status = {
4750+ 'is_error': '', 'is_online': '', 'is_connected': 'True',
4751+ 'name': 'CHECK_VERSION', 'connection': '',
4752+ 'queues': 'WORKING_ON_CONTENT',
4753+ 'description': 'something nicer',
4754+ }
4755+ yield self.assert_correct_status(FILE_SYNC_SYNCING)
4756+
4757+ @inlineCallbacks
4758+ def test_idle(self):
4759+ """The syncdaemon status is processed and emitted."""
4760+ MockDBusClient.status = {
4761+ 'is_error': '', 'is_online': 'True', 'is_connected': 'True',
4762+ 'name': 'QUEUE_MANAGER', 'connection': '',
4763+ 'queues': 'IDLE',
4764+ 'description': 'something nice',
4765+ }
4766+ yield self.assert_correct_status(FILE_SYNC_IDLE)
4767+
4768+ @inlineCallbacks
4769+ def test_unknown(self):
4770+ """The syncdaemon status is processed and emitted."""
4771+ MockDBusClient.status = {
4772+ 'is_error': '', 'is_online': '', 'is_connected': '',
4773+ 'name': '', 'connection': '', 'queues': '',
4774+ 'description': '',
4775+ }
4776+ yield self.assert_correct_status(FILE_SYNC_UNKNOWN)
4777+
4778+ has_warning = self.memento.check_warning('file_sync_status: unknown',
4779+ repr(MockDBusClient.status))
4780+ self.assertTrue(has_warning)
4781+
4782+ def test_status_changed(self):
4783+ """The file_sync_status is the status changed handler."""
4784+ self.be.status_changed_handler = self._set_called
4785+ status = {'name': 'foo', 'description': 'bar', 'is_error': '',
4786+ 'is_connected': '', 'is_online': '', 'queues': ''}
4787+ # pylint: disable=E1101
4788+ backend.dbus_client.status_changed_handler(status)
4789+
4790+ # pylint: disable=W0212
4791+ # Access to a protected member _process_file_sync_status
4792+ expected_status = self.be._process_file_sync_status(status)
4793+ self.assertEqual(self._called, ((expected_status,), {}))
4794
4795=== modified file 'ubuntuone/controlpanel/utils.py'
4796--- ubuntuone/controlpanel/utils.py 2010-12-06 12:27:11 +0000
4797+++ ubuntuone/controlpanel/utils.py 2010-12-22 14:37:52 +0000
4798@@ -20,8 +20,6 @@
4799
4800 import os
4801
4802-from functools import wraps
4803-
4804 import dbus
4805
4806 from ubuntuone.controlpanel.logger import setup_logging
4807@@ -68,24 +66,6 @@
4808 return os.path.join(get_project_dir(), filename)
4809
4810
4811-def log_call(log_func):
4812- """Decorator to add log info using 'log_func'."""
4813-
4814- def middle(f):
4815- """Add logging when calling 'f'."""
4816-
4817- @wraps(f)
4818- def inner(*args, **kwargs):
4819- """Call f(*args, **kwargs)."""
4820- log_func('%s: args %r, kwargs %r.', f.__name__, args, kwargs)
4821- res = f(*args, **kwargs)
4822- return res
4823-
4824- return inner
4825-
4826- return middle
4827-
4828-
4829 def is_dbus_no_reply(failure):
4830 """Decide if 'failure' is a DBus NoReply Error."""
4831 exc = failure.value
4832
4833=== modified file 'ubuntuone/controlpanel/webclient.py'
4834--- ubuntuone/controlpanel/webclient.py 2010-12-06 12:27:11 +0000
4835+++ ubuntuone/controlpanel/webclient.py 2010-12-22 14:37:52 +0000
4836@@ -48,22 +48,24 @@
4837
4838 def _handler(self, session, msg, d):
4839 """Handle the result of an http message."""
4840- logger.debug("WebClient: got http response: %d", msg.status_code)
4841+ logger.debug("WebClient: got http response %d for uri %r",
4842+ msg.status_code, msg.get_uri().to_string(False))
4843+ data = msg.response_body.data
4844 if msg.status_code == 200:
4845- result = simplejson.loads(msg.response_body.data)
4846+ result = simplejson.loads(data)
4847 d.callback(result)
4848 else:
4849- e = WebClientError(msg.status_code, msg)
4850+ e = WebClientError(msg.status_code, data)
4851 d.errback(e)
4852
4853 def _call_api_with_creds(self, credentials, api_name):
4854 """Get a given url from the webservice with credentials."""
4855- url = self.base_url + api_name
4856+ url = (self.base_url + api_name).encode('utf-8')
4857 method = "GET"
4858+ logger.debug("WebClient: getting url: %s, %s", method, url)
4859 msg = Soup.Message.new(method, url)
4860 add_oauth_headers(msg.request_headers.append, method, url, credentials)
4861 d = defer.Deferred()
4862- logger.debug("WebClient: getting url: %s", url)
4863 self.session.queue_message(msg, self._handler, d)
4864 return d
4865

Subscribers

People subscribed via source and target branches