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

Proposed by Natalia Bidart
Status: Merged
Merge reported by: Sebastien Bacher
Merged at revision: not available
Proposed branch: lp:~nataliabidart/ubuntu/natty/ubuntuone-control-panel/ubuntuone-control-panel-0.9.5
Merge into: lp:ubuntu/natty/ubuntuone-control-panel
Diff against target: 2658 lines (+1102/-207)
26 files modified
PKG-INFO (+1/-1)
bin/ubuntuone-control-panel-gtk (+8/-20)
debian/changelog (+42/-0)
po/POTFILES.in (+1/-0)
run-tests (+5/-15)
setup.py (+1/-1)
ubuntuone-control-panel-gtk.desktop.in (+0/-6)
ubuntuone/controlpanel/backend.py (+154/-49)
ubuntuone/controlpanel/dbus_client.py (+0/-1)
ubuntuone/controlpanel/dbus_service.py (+55/-34)
ubuntuone/controlpanel/gtk/__init__.py (+4/-2)
ubuntuone/controlpanel/gtk/gui.py (+184/-35)
ubuntuone/controlpanel/gtk/package_manager.py (+3/-2)
ubuntuone/controlpanel/gtk/tests/__init__.py (+13/-1)
ubuntuone/controlpanel/gtk/tests/test_gui.py (+76/-1)
ubuntuone/controlpanel/gtk/tests/test_gui_basic.py (+129/-5)
ubuntuone/controlpanel/gtk/tests/test_package_manager.py (+2/-2)
ubuntuone/controlpanel/integrationtests/__init__.py (+1/-2)
ubuntuone/controlpanel/integrationtests/test_dbus_service.py (+67/-3)
ubuntuone/controlpanel/integrationtests/test_gui_service.py (+98/-0)
ubuntuone/controlpanel/integrationtests/test_webclient.py (+11/-1)
ubuntuone/controlpanel/replication_client.py (+1/-1)
ubuntuone/controlpanel/tests/__init__.py (+24/-12)
ubuntuone/controlpanel/tests/test_backend.py (+205/-8)
ubuntuone/controlpanel/utils.py (+1/-1)
ubuntuone/controlpanel/webclient.py (+16/-4)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu/natty/ubuntuone-control-panel/ubuntuone-control-panel-0.9.5
Reviewer Review Type Date Requested Status
Ubuntu Sponsors Pending
Review via email: mp+56995@code.launchpad.net

Description of the change

  * New upstream release:

    [ <email address hidden> ]
      - Now that we set the launcher urgency from ubuntuone-client, the control
      panel needs to remove it when its window receives focus (LP: #747677).
      - changed default value for switch_to to empty string, and now don't call
      switch_to method when the value is empty string (or anything else falsy)
      (LP: #752943).
      - Added proper defaults to the command line arguments (LP: #746489).
      - Fixed issue where closing the panel resulted in a runtime error
      (LP: #745987).
      - This adds a method to the dbus service that allows switching between
      panels, and/or drawing attention to the control panel (LP: #742008).
      - Removed the shortcut group that causes two Ubuntu One entries to appear
      in the messaging menu when syncdaemon is not running (LP: #721525).
    [ Natalia B. Bidart <email address hidden> ]
      - If servers reply with a 401, clear credentials and ask user to
      authenticate (LP: #726612).
      - Moving style_check down so the exit code from u1trial is not hidden by
      && operator.
      - Unify disable/enable file sync functionality among Services tab and
      global file sync status (LP: #729301).
      - Cloud Folders tab is now disabled when the file sync service is
      (LP: #747482).
      - Improving legend for plugin installation to ease translations
      (LP: #746374).
      - Added volumes.ui to the translation list (LP: #746370).
      - Small improvement to show something else besides the generic "Value can
      not be retrieved." error (LP: #722485).
      - Made the backend robust against possible None values (or any non
      basestring instance) sent from the API server (LP: #745790).
      - Decoupled device list retrieved from the web from the local settings
      retrieved from syncdaemon (LP: #720704).
      - Stop the control panel backend once the UI is done
      (LP: #704434).
      - After initial computer adding, syncdameon is asked to connect
      (LP: #715873).

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

Fixing editor's email address.

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 2011-03-23 20:33:42 +0000
3+++ PKG-INFO 2011-04-08 19:43:13 +0000
4@@ -1,6 +1,6 @@
5 Metadata-Version: 1.1
6 Name: ubuntuone-control-panel
7-Version: 0.9.4
8+Version: 0.9.5
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-gtk'
14--- bin/ubuntuone-control-panel-gtk 2011-03-23 16:06:41 +0000
15+++ bin/ubuntuone-control-panel-gtk 2011-04-08 19:43:13 +0000
16@@ -2,6 +2,7 @@
17 # -*- coding: utf-8 -*-
18
19 # Authors: Natalia B Bidart <natalia.bidart@canonical.com>
20+# Eric Casteleijn <eric.casteleijn@canonical.com>
21 #
22 # Copyright 2010 Canonical Ltd.
23 #
24@@ -20,20 +21,16 @@
25
26 # Invalid name "ubuntuone-control-panel-gtk", pylint: disable=C0103
27
28+import gettext
29 import sys
30
31-import dbus.mainloop.glib
32-import gettext
33-
34 from optparse import OptionParser
35
36-from ubuntuone.controlpanel.gtk import DBUS_BUS_NAME, TRANSLATION_DOMAIN
37+from ubuntuone.controlpanel.gtk import TRANSLATION_DOMAIN
38
39-dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
40 gettext.textdomain(TRANSLATION_DOMAIN)
41-
42 # import the GUI after the translation domain has been set
43-from ubuntuone.controlpanel.gtk.gui import ControlPanelWindow
44+from ubuntuone.controlpanel.gtk.gui import main
45
46
47 def parser_options():
48@@ -41,26 +38,17 @@
49 usage = "Usage: %prog [option]"
50 result = OptionParser(usage=usage)
51 result.add_option("", "--switch-to", dest="switch_to", type="string",
52- metavar="PANEL_NAME",
53+ metavar="PANEL_NAME", default="",
54 help="Start the Ubuntu One Control Panel (GTK) in the "
55 "PANEL_NAME tab. Possible values are: "
56 "dashboard, volumes, devices, applications")
57 result.add_option("-a", "--alert", dest="alert", action="store_true",
58- help="Start the Ubuntu One Control Panel (GTK) alerting "
59- "the user to its presence.")
60+ default=False, help="Start the Ubuntu One Control Panel "
61+ "(GTK) alerting the user to its presence.")
62 return result
63
64
65 if __name__ == "__main__":
66- bus = dbus.SessionBus()
67- name = bus.request_name(DBUS_BUS_NAME,
68- dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
69- if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
70- sys.exit(0)
71-
72- bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
73 parser = parser_options()
74 (options, args) = parser.parse_args(sys.argv)
75- gui = ControlPanelWindow(
76- switch_to=options.switch_to, alert=options.alert)
77- gui.main()
78+ main(switch_to=options.switch_to, alert=options.alert)
79
80=== modified file 'debian/changelog'
81--- debian/changelog 2011-03-23 20:44:08 +0000
82+++ debian/changelog 2011-04-08 19:43:13 +0000
83@@ -1,3 +1,45 @@
84+ubuntuone-control-panel (0.9.5-0ubuntu1) UNRELEASED; urgency=low
85+
86+ * New upstream release:
87+
88+ [ eric.casteleijn@canonical.com ]
89+ - Now that we set the launcher urgency from ubuntuone-client, the control
90+ panel needs to remove it when its window receives focus (LP: #747677).
91+ - changed default value for switch_to to empty string, and now don't call
92+ switch_to method when the value is empty string (or anything else falsy)
93+ (LP: #752943).
94+ - Added proper defaults to the command line arguments (LP: #746489).
95+ - Fixed issue where closing the panel resulted in a runtime error
96+ (LP: #745987).
97+ - This adds a method to the dbus service that allows switching between
98+ panels, and/or drawing attention to the control panel (LP: #742008).
99+ - Removed the shortcut group that causes two Ubuntu One entries to appear
100+ in the messaging menu when syncdaemon is not running (LP: #721525).
101+ [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
102+ - If servers reply with a 401, clear credentials and ask user to
103+ authenticate (LP: #726612).
104+ - Moving style_check down so the exit code from u1trial is not hidden by
105+ && operator.
106+ - Unify disable/enable file sync functionality among Services tab and
107+ global file sync status (LP: #729301).
108+ - Cloud Folders tab is now disabled when the file sync service is
109+ (LP: #747482).
110+ - Improving legend for plugin installation to ease translations
111+ (LP: #746374).
112+ - Added volumes.ui to the translation list (LP: #746370).
113+ - Small improvement to show something else besides the generic "Value can
114+ not be retrieved." error (LP: #722485).
115+ - Made the backend robust against possible None values (or any non
116+ basestring instance) sent from the API server (LP: #745790).
117+ - Decoupled device list retrieved from the web from the local settings
118+ retrieved from syncdaemon (LP: #720704).
119+ - Stop the control panel backend once the UI is done
120+ (LP: #704434).
121+ - After initial computer adding, syncdameon is asked to connect
122+ (LP: #715873).
123+
124+ -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Fri, 08 Apr 2011 15:53:02 -0300
125+
126 ubuntuone-control-panel (0.9.4-0ubuntu1) natty; urgency=low
127
128 * New upstream release:
129
130=== modified file 'po/POTFILES.in'
131--- po/POTFILES.in 2011-01-07 20:07:39 +0000
132+++ po/POTFILES.in 2011-04-08 19:43:13 +0000
133@@ -7,3 +7,4 @@
134 [type: gettext/glade] data/management.ui
135 [type: gettext/glade] data/overview.ui
136 [type: gettext/glade] data/services.ui
137+[type: gettext/glade] data/volumes.ui
138
139=== modified file 'run-tests'
140--- run-tests 2010-12-06 12:27:11 +0000
141+++ run-tests 2011-04-08 19:43:13 +0000
142@@ -19,19 +19,8 @@
143 set -e
144
145 if [ $# -ne 0 ]; then
146- # an extra argument was given
147- if [ $1 == "--integration" ]; then
148- # run only integration tests
149- MODULE="ubuntuone/controlpanel/integrationtests"
150- else
151- if [ $1 == "--unittests" ]; then
152- # run only non-integration tests (unittests)
153- MODULE="ubuntuone/controlpanel/tests"
154- else
155- # run specific module given by the caller
156- MODULE="$@"
157- fi
158- fi
159+ # run specific module given by the caller
160+ MODULE="$@"
161 else
162 # run all tests, useful for tarmac and reviews
163 MODULE="ubuntuone/controlpanel"
164@@ -40,12 +29,13 @@
165 style_check() {
166 pylint ubuntuone/
167 if [ -x `which pep8` ]; then
168- pep8 --repeat ubuntuone/
169+ pep8 --repeat bin/ $MODULE
170 else
171 echo "Please install the 'pep8' package."
172 fi
173 }
174
175 echo "Running test suite for ""$MODULE"
176-`which xvfb-run` u1trial "$MODULE" && style_check
177+`which xvfb-run` u1trial "$MODULE"
178+style_check
179 rm -rf _trial_temp
180
181=== modified file 'setup.py'
182--- setup.py 2011-03-23 20:33:42 +0000
183+++ setup.py 2011-04-08 19:43:13 +0000
184@@ -79,7 +79,7 @@
185
186 DistUtilsExtra.auto.setup(
187 name='ubuntuone-control-panel',
188- version='0.9.4',
189+ version='0.9.5',
190 license='GPL v3',
191 author='Natalia Bidart',
192 author_email='natalia.bidart@canonical.com',
193
194=== modified file 'ubuntuone-control-panel-gtk.desktop.in'
195--- ubuntuone-control-panel-gtk.desktop.in 2011-02-23 13:57:42 +0000
196+++ ubuntuone-control-panel-gtk.desktop.in 2011-04-08 19:43:13 +0000
197@@ -7,10 +7,4 @@
198 Type=Application
199 Categories=GNOME;GTK;Settings;
200 StartupNotify=true
201-X-Ayatana-Desktop-Shortcuts=U1
202 X-Ayatana-Appmenu-Show-Stubs=False
203-
204-[U1 Shortcut Group]
205-Name=Ubuntu One
206-Exec=ubuntuone-control-panel-gtk
207-OnlyShowIn=Messaging Menu
208
209=== modified file 'ubuntuone/controlpanel/backend.py'
210--- ubuntuone/controlpanel/backend.py 2011-03-10 02:59:44 +0000
211+++ ubuntuone/controlpanel/backend.py 2011-04-08 19:43:13 +0000
212@@ -20,14 +20,17 @@
213 """A backend for the Ubuntu One Control Panel."""
214
215 from collections import defaultdict
216+from functools import wraps
217
218 from twisted.internet.defer import inlineCallbacks, returnValue
219
220 from ubuntuone.controlpanel import dbus_client
221 from ubuntuone.controlpanel import replication_client
222 from ubuntuone.controlpanel.logger import setup_logging, log_call
223-from ubuntuone.controlpanel.webclient import WebClient
224-
225+# pylint: disable=W0611
226+from ubuntuone.controlpanel.webclient import (UnauthorizedError,
227+ WebClient, WebClientError)
228+# pylint: enable=W0611
229
230 logger = setup_logging('backend')
231
232@@ -61,6 +64,35 @@
233 return 'True' if value else ''
234
235
236+def filter_field(info, field):
237+ """Return a copy of 'info' where each item has 'field' hidden."""
238+ result = []
239+ for item in info:
240+ item = item.copy()
241+ item[field] = '<hidden>'
242+ result.append(item)
243+ return result
244+
245+
246+def process_unauthorized(f):
247+ """Decorator to catch UnauthorizedError from the webclient and act upon."""
248+
249+ @inlineCallbacks
250+ @wraps(f)
251+ def inner(*args, **kwargs):
252+ """Handle UnauthorizedError and clear credentials."""
253+ try:
254+ result = yield f(*args, **kwargs)
255+ except UnauthorizedError, e:
256+ logger.exception('process_unauthorized (clearing credentials):')
257+ yield dbus_client.clear_credentials()
258+ raise e
259+
260+ returnValue(result)
261+
262+ return inner
263+
264+
265 class ControlBackend(object):
266 """The control panel backend."""
267
268@@ -68,14 +100,17 @@
269 FOLDER_TYPE = u'UDF'
270 SHARE_TYPE = u'SHARE'
271 NAME_NOT_SET = u'ENAMENOTSET'
272+ STATUS_DISABLED = {MSG_KEY: '', STATUS_KEY: FILE_SYNC_DISABLED}
273
274- def __init__(self):
275+ def __init__(self, shutdown_func=None):
276 """Initialize the webclient."""
277+ self.shutdown_func = shutdown_func
278 self.wc = WebClient(dbus_client.get_credentials)
279 self._status_changed_handler = None
280 self.status_changed_handler = lambda *a: None
281
282 self._volumes = {} # cache last known volume info
283+ self.file_sync_disabled = False
284
285 def _process_file_sync_status(self, status):
286 """Process raw file sync status into custom format.
287@@ -89,7 +124,8 @@
288
289 """
290 if not status:
291- return {MSG_KEY: '', STATUS_KEY: FILE_SYNC_DISABLED}
292+ self.file_sync_disabled = True
293+ return self.STATUS_DISABLED
294
295 msg = '%s (%s)' % (status['description'], status['name'])
296 result = {MSG_KEY: msg}
297@@ -113,6 +149,7 @@
298 elif is_disconnected:
299 result[STATUS_KEY] = FILE_SYNC_DISCONNECTED
300 elif is_starting:
301+ self.file_sync_disabled = False
302 result[STATUS_KEY] = FILE_SYNC_STARTING
303 elif is_stopped:
304 result[STATUS_KEY] = FILE_SYNC_STOPPED
305@@ -120,7 +157,10 @@
306 logger.warning('file_sync_status: unknown (got %r)', status)
307 result[STATUS_KEY] = FILE_SYNC_UNKNOWN
308
309- return result
310+ if self.file_sync_disabled:
311+ return self.STATUS_DISABLED
312+ else:
313+ return result
314
315 def _set_status_changed_handler(self, handler):
316 """Set 'handler' to be called when file sync status changes."""
317@@ -142,6 +182,56 @@
318 _set_status_changed_handler)
319
320 @inlineCallbacks
321+ def _process_device_web_info(self, devices, enabled, limit_bw, limits,
322+ show_notifs):
323+ """Return a lis of processed devices."""
324+ result = []
325+ for d in devices:
326+ di = {}
327+ di["type"] = d["kind"]
328+ di["name"] = d["description"]
329+ di["configurable"] = ''
330+ if di["type"] == DEVICE_TYPE_COMPUTER:
331+ di["device_id"] = di["type"] + d["token"]
332+ if di["type"] == DEVICE_TYPE_PHONE:
333+ di["device_id"] = di["type"] + str(d["id"])
334+
335+ is_local = yield self.device_is_local(di["device_id"])
336+ di["is_local"] = bool_str(is_local)
337+ # currently, only local devices are configurable.
338+ # eventually, more devices will be configurable.
339+ di["configurable"] = bool_str(is_local and enabled)
340+
341+ if bool(di["configurable"]):
342+ di["limit_bandwidth"] = bool_str(limit_bw)
343+ di["show_all_notifications"] = bool_str(show_notifs)
344+ upload = limits["upload"]
345+ download = limits["download"]
346+ di[UPLOAD_KEY] = str(upload)
347+ di[DOWNLOAD_KEY] = str(download)
348+
349+ # date_added is not in the webservice yet (LP: #673668)
350+ # di["date_added"] = ""
351+
352+ # missing values (LP: #673668)
353+ # di["available_services"] = ""
354+ # di["enabled_services"] = ""
355+
356+ # make a sanity check
357+ for key, val in di.iteritems():
358+ if not isinstance(val, basestring):
359+ logger.warning('_process_device_web_info: (key %r), '
360+ 'val %r is not a basestring.', key, val)
361+ di[key] = repr(val)
362+
363+ if is_local: # prepend the local device!
364+ result.insert(0, di)
365+ else:
366+ result.append(di)
367+
368+ returnValue(result)
369+
370+ @inlineCallbacks
371 def get_token(self):
372 """Return the token from the credentials."""
373 credentials = yield dbus_client.get_credentials()
374@@ -153,10 +243,10 @@
375 dtype, did = self.type_n_id(device_id)
376 local_token = yield self.get_token()
377 is_local = (dtype == DEVICE_TYPE_COMPUTER and did == local_token)
378- logger.info('device_is_local: result %r, ', is_local)
379 returnValue(is_local)
380
381 @log_call(logger.debug)
382+ @process_unauthorized
383 @inlineCallbacks
384 def account_info(self):
385 """Get the user account info."""
386@@ -184,50 +274,55 @@
387 returnValue(result)
388
389 @log_call(logger.debug)
390+ @process_unauthorized
391 @inlineCallbacks
392 def devices_info(self):
393 """Get the user devices info."""
394- result = []
395- limit_bw = yield dbus_client.bandwidth_throttling_enabled()
396- show_all_notif = yield dbus_client.show_all_notifications_enabled()
397- limits = yield dbus_client.get_throttling_limits()
398-
399- devices = yield self.wc.call_api(DEVICES_API)
400- for d in devices:
401- di = {}
402- di["type"] = d["kind"]
403- di["name"] = d["description"]
404- di["configurable"] = ''
405- if di["type"] == DEVICE_TYPE_COMPUTER:
406- di["device_id"] = di["type"] + d["token"]
407- if di["type"] == DEVICE_TYPE_PHONE:
408- di["device_id"] = di["type"] + str(d["id"])
409-
410- is_local = yield self.device_is_local(di["device_id"])
411- di["is_local"] = bool_str(is_local)
412- # currently, only local devices are configurable.
413- # eventually, more the devices will be configurable.
414- di["configurable"] = bool_str(is_local)
415-
416- if bool(di["configurable"]):
417- di["limit_bandwidth"] = bool_str(limit_bw)
418- di["show_all_notifications"] = bool_str(show_all_notif)
419+ result = limit_bw = limits = show_notifs = None
420+ enabled = yield dbus_client.files_sync_enabled()
421+ if enabled:
422+ limit_bw = yield dbus_client.bandwidth_throttling_enabled()
423+ show_notifs = yield dbus_client.show_all_notifications_enabled()
424+ limits = yield dbus_client.get_throttling_limits()
425+
426+ logger.debug('devices_info: file sync enabled? %s limit_bw %s, limits '
427+ '%s, show_notifs %s',
428+ enabled, limit_bw, limits, show_notifs)
429+
430+ try:
431+ devices = yield self.wc.call_api(DEVICES_API)
432+ except UnauthorizedError:
433+ raise
434+ except WebClientError:
435+ logger.exception('devices_info: web client failure:')
436+ else:
437+ result = yield self._process_device_web_info(devices, enabled,
438+ limit_bw, limits,
439+ show_notifs)
440+ if result is None:
441+ logger.info('devices_info: result is None after calling '
442+ 'devices/ API, building the local device.')
443+ credentials = yield dbus_client.get_credentials()
444+ local_device = {}
445+ local_device["type"] = DEVICE_TYPE_COMPUTER
446+ local_device["name"] = credentials['name']
447+ device_id = local_device["type"] + credentials["token"]
448+ local_device["device_id"] = device_id
449+ local_device["is_local"] = bool_str(True)
450+ local_device["configurable"] = bool_str(enabled)
451+ if bool(local_device["configurable"]):
452+ local_device["limit_bandwidth"] = bool_str(limit_bw)
453+ show_notifs = bool_str(show_notifs)
454+ local_device["show_all_notifications"] = show_notifs
455 upload = limits["upload"]
456 download = limits["download"]
457- di[UPLOAD_KEY] = str(upload)
458- di[DOWNLOAD_KEY] = str(download)
459-
460- # date_added is not in the webservice yet (LP: #673668)
461- # di["date_added"] = ""
462-
463- # missing values (LP: #673668)
464- # di["available_services"] = ""
465- # di["enabled_services"] = ""
466-
467- if is_local: # prepend the local device!
468- result.insert(0, di)
469- else:
470- result.append(di)
471+ local_device[UPLOAD_KEY] = str(upload)
472+ local_device[DOWNLOAD_KEY] = str(download)
473+ result = [local_device]
474+ else:
475+ logger.info('devices_info: result is not None after calling '
476+ 'devices/ API: %r',
477+ filter_field(result, field='device_id'))
478
479 returnValue(result)
480
481@@ -239,7 +334,7 @@
482 return DEVICE_TYPE_PHONE, device_id[5:]
483 return "No device", device_id
484
485- @log_call(logger.info)
486+ @log_call(logger.info, with_args=False)
487 @inlineCallbacks
488 def change_device_settings(self, device_id, settings):
489 """Change the settings for the given device."""
490@@ -275,7 +370,8 @@
491 # still pending: more work on the settings dict (LP: #673674)
492 returnValue(device_id)
493
494- @log_call(logger.warning)
495+ @log_call(logger.warning, with_args=False)
496+ @process_unauthorized
497 @inlineCallbacks
498 def remove_device(self, device_id):
499 """Remove a device's tokens from the sso server."""
500@@ -286,8 +382,8 @@
501 yield self.wc.call_api(api)
502
503 if is_local:
504- logger.warning('remove_device: device is local, id %r, '
505- 'clearing credentials.', device_id)
506+ logger.warning('remove_device: device is local! removing and '
507+ 'clearing credentials.')
508 yield dbus_client.clear_credentials()
509
510 returnValue(device_id)
511@@ -308,12 +404,14 @@
512 def enable_files(self):
513 """Enable the files service."""
514 yield dbus_client.set_files_sync_enabled(True)
515+ self.file_sync_disabled = False
516
517 @log_call(logger.debug)
518 @inlineCallbacks
519 def disable_files(self):
520 """Enable the files service."""
521 yield dbus_client.set_files_sync_enabled(False)
522+ self.file_sync_disabled = True
523
524 @log_call(logger.debug)
525 @inlineCallbacks
526@@ -478,3 +576,10 @@
527 """Install the extension to sync bookmarks."""
528 # still pending (LP: #673673)
529 returnValue(None)
530+
531+ @log_call(logger.info)
532+ def shutdown(self):
533+ """Stop this service."""
534+ # do any other needed cleanup
535+ if self.shutdown_func is not None:
536+ self.shutdown_func()
537
538=== modified file 'ubuntuone/controlpanel/dbus_client.py'
539--- ubuntuone/controlpanel/dbus_client.py 2011-02-23 13:57:42 +0000
540+++ ubuntuone/controlpanel/dbus_client.py 2011-04-08 19:43:13 +0000
541@@ -64,7 +64,6 @@
542
543 def found_credentials(app_name, creds):
544 """Credentials have been found."""
545- logger.debug('credentials were found for app_name %r.', app_name)
546 if app_name == APP_NAME:
547 logger.info('credentials were found! (%r).', APP_NAME)
548 d.callback(creds)
549
550=== modified file 'ubuntuone/controlpanel/dbus_service.py'
551--- ubuntuone/controlpanel/dbus_service.py 2011-01-25 19:08:59 +0000
552+++ ubuntuone/controlpanel/dbus_service.py 2011-04-08 19:43:13 +0000
553@@ -32,7 +32,8 @@
554 from ubuntuone.controlpanel import (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH,
555 DBUS_PREFERENCES_IFACE)
556 from ubuntuone.controlpanel.backend import (
557- ControlBackend, FILE_SYNC_DISABLED, FILE_SYNC_DISCONNECTED,
558+ ControlBackend, filter_field, UnauthorizedError,
559+ FILE_SYNC_DISABLED, FILE_SYNC_DISCONNECTED,
560 FILE_SYNC_ERROR, FILE_SYNC_IDLE, FILE_SYNC_STARTING, FILE_SYNC_STOPPED,
561 FILE_SYNC_SYNCING,
562 MSG_KEY, STATUS_KEY,
563@@ -77,7 +78,7 @@
564 return result
565
566
567-def transform_failure(f):
568+def transform_failure(f, auth_error=None):
569 """Decorator to apply to DBus error signals.
570
571 With this call, a Failure is transformed into a string-string dict.
572@@ -85,8 +86,11 @@
573 """
574 def inner(error, _=None):
575 """Do the Failure transformation."""
576+ logger.error('processing failure: %r', error.printTraceback())
577 error_dict = error_handler(error)
578- if _ is not None:
579+ if auth_error is not None and error.check(UnauthorizedError):
580+ result = auth_error(error_dict)
581+ elif _ is not None:
582 result = f(_, error_dict)
583 else:
584 result = f(error_dict)
585@@ -115,19 +119,25 @@
586 super(ControlPanelBackend, self).__init__(*args, **kwargs)
587 self.backend = backend
588 self.backend.status_changed_handler = self.process_status
589+ self.transform = lambda f: transform_failure(f, self.UnauthorizedError)
590 logger.debug('ControlPanelBackend: created with %r, %r.\n'
591 'status_changed_handler is %r.',
592 args, kwargs, self.process_status)
593
594 # pylint: disable=C0103
595
596+ @log_call(logger.error)
597+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
598+ def UnauthorizedError(self, error):
599+ """The credentials are not valid."""
600+
601 @log_call(logger.debug)
602 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
603 def account_info(self):
604 """Find out the account info for the current logged in user."""
605 d = self.backend.account_info()
606 d.addCallback(self.AccountInfoReady)
607- d.addErrback(transform_failure(self.AccountInfoError))
608+ d.addErrback(self.transform(self.AccountInfoError))
609
610 @log_call(logger.debug)
611 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
612@@ -147,12 +157,13 @@
613 """Find out the devices info for the logged in user."""
614 d = self.backend.devices_info()
615 d.addCallback(self.DevicesInfoReady)
616- d.addErrback(transform_failure(self.DevicesInfoError))
617+ d.addErrback(self.transform(self.DevicesInfoError))
618
619- @log_call(logger.debug)
620 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")
621 def DevicesInfoReady(self, info):
622 """The info for the devices is available right now."""
623+ logger.debug('DevicesInfoReady: args %r',
624+ filter_field(info, field='device_id'))
625
626 @log_call(logger.error)
627 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
628@@ -161,41 +172,41 @@
629
630 #---
631
632- @log_call(logger.info)
633+ @log_call(logger.info, with_args=False)
634 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="sa{ss}")
635 def change_device_settings(self, device_id, settings):
636 """Configure a given device."""
637 d = self.backend.change_device_settings(device_id, settings)
638 d.addCallback(self.DeviceSettingsChanged)
639- d.addErrback(transform_failure(self.DeviceSettingsChangeError),
640+ d.addErrback(self.transform(self.DeviceSettingsChangeError),
641 device_id)
642
643- @log_call(logger.info)
644+ @log_call(logger.info, with_args=False)
645 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
646 def DeviceSettingsChanged(self, device_id):
647 """The settings for the device were changed."""
648
649- @log_call(logger.error)
650+ @log_call(logger.error, with_args=False)
651 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")
652 def DeviceSettingsChangeError(self, device_id, error):
653 """Problem changing settings for the device."""
654
655 #---
656
657- @log_call(logger.warning)
658+ @log_call(logger.warning, with_args=False)
659 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="s")
660 def remove_device(self, device_id):
661 """Remove a given device."""
662 d = self.backend.remove_device(device_id)
663 d.addCallback(self.DeviceRemoved)
664- d.addErrback(transform_failure(self.DeviceRemovalError), device_id)
665+ d.addErrback(self.transform(self.DeviceRemovalError), device_id)
666
667- @log_call(logger.warning)
668+ @log_call(logger.warning, with_args=False)
669 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
670 def DeviceRemoved(self, device_id):
671 """The removal for the device was completed."""
672
673- @log_call(logger.error)
674+ @log_call(logger.error, with_args=False)
675 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")
676 def DeviceRemovalError(self, device_id, error):
677 """Problem removing the device."""
678@@ -234,7 +245,7 @@
679 """Get the status of the file sync service."""
680 d = self.backend.file_sync_status()
681 d.addCallback(self.process_status)
682- d.addErrback(transform_failure(self.FileSyncStatusError))
683+ d.addErrback(self.transform(self.FileSyncStatusError))
684
685 @log_call(logger.debug)
686 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
687@@ -284,7 +295,7 @@
688 """Enable the files service."""
689 d = self.backend.enable_files()
690 d.addCallback(lambda _: self.FilesEnabled())
691- d.addErrback(transform_failure(self.FilesEnableError))
692+ d.addErrback(self.transform(self.FilesEnableError))
693
694 @log_call(logger.debug)
695 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
696@@ -304,7 +315,7 @@
697 """Disable the files service."""
698 d = self.backend.disable_files()
699 d.addCallback(lambda _: self.FilesDisabled())
700- d.addErrback(transform_failure(self.FilesDisableError))
701+ d.addErrback(self.transform(self.FilesDisableError))
702
703 @log_call(logger.debug)
704 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
705@@ -324,7 +335,7 @@
706 """Connect the files service."""
707 d = self.backend.connect_files()
708 d.addCallback(lambda _: self.FilesConnected())
709- d.addErrback(transform_failure(self.FilesConnectError))
710+ d.addErrback(self.transform(self.FilesConnectError))
711
712 @log_call(logger.debug)
713 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
714@@ -344,7 +355,7 @@
715 """Disconnect the files service."""
716 d = self.backend.disconnect_files()
717 d.addCallback(lambda _: self.FilesDisconnected())
718- d.addErrback(transform_failure(self.FilesDisconnectError))
719+ d.addErrback(self.transform(self.FilesDisconnectError))
720
721 @log_call(logger.debug)
722 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
723@@ -364,7 +375,7 @@
724 """Restart the files service."""
725 d = self.backend.restart_files()
726 d.addCallback(lambda _: self.FilesRestarted())
727- d.addErrback(transform_failure(self.FilesRestartError))
728+ d.addErrback(self.transform(self.FilesRestartError))
729
730 @log_call(logger.debug)
731 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
732@@ -384,7 +395,7 @@
733 """Start the files service."""
734 d = self.backend.start_files()
735 d.addCallback(lambda _: self.FilesStarted())
736- d.addErrback(transform_failure(self.FilesStartError))
737+ d.addErrback(self.transform(self.FilesStartError))
738
739 @log_call(logger.debug)
740 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
741@@ -404,7 +415,7 @@
742 """Stop the files service."""
743 d = self.backend.stop_files()
744 d.addCallback(lambda _: self.FilesStopped())
745- d.addErrback(transform_failure(self.FilesStopError))
746+ d.addErrback(self.transform(self.FilesStopError))
747
748 @log_call(logger.debug)
749 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
750@@ -424,7 +435,7 @@
751 """Find out the volumes info for the logged in user."""
752 d = self.backend.volumes_info()
753 d.addCallback(self.VolumesInfoReady)
754- d.addErrback(transform_failure(self.VolumesInfoError))
755+ d.addErrback(self.transform(self.VolumesInfoError))
756
757 @log_call(logger.debug)
758 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a(ssaa{ss})")
759@@ -444,7 +455,7 @@
760 """Configure a given volume."""
761 d = self.backend.change_volume_settings(volume_id, settings)
762 d.addCallback(self.VolumeSettingsChanged)
763- d.addErrback(transform_failure(self.VolumeSettingsChangeError),
764+ d.addErrback(self.transform(self.VolumeSettingsChangeError),
765 volume_id)
766
767 @log_call(logger.info)
768@@ -465,7 +476,7 @@
769 """Return the replications info."""
770 d = self.backend.replications_info()
771 d.addCallback(self.ReplicationsInfoReady)
772- d.addErrback(transform_failure(self.ReplicationsInfoError))
773+ d.addErrback(self.transform(self.ReplicationsInfoError))
774
775 @log_call(logger.debug)
776 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")
777@@ -485,7 +496,7 @@
778 """Configure a given replication."""
779 d = self.backend.change_replication_settings(replication_id, settings)
780 d.addCallback(self.ReplicationSettingsChanged)
781- d.addErrback(transform_failure(self.ReplicationSettingsChangeError),
782+ d.addErrback(self.transform(self.ReplicationSettingsChangeError),
783 replication_id)
784
785 @log_call(logger.info)
786@@ -506,7 +517,7 @@
787 """Check if the extension to sync bookmarks is installed."""
788 d = self.backend.query_bookmark_extension()
789 d.addCallback(self.QueryBookmarksResult)
790- d.addErrback(transform_failure(self.QueryBookmarksError))
791+ d.addErrback(self.transform(self.QueryBookmarksError))
792
793 @log_call(logger.debug)
794 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="b")
795@@ -526,7 +537,7 @@
796 """Install the extension to sync bookmarks."""
797 d = self.backend.install_bookmarks_extension()
798 d.addCallback(lambda _: self.InstallBookmarksSuccess())
799- d.addErrback(transform_failure(self.InstallBookmarksError))
800+ d.addErrback(self.transform(self.InstallBookmarksError))
801
802 @log_call(logger.info)
803 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="")
804@@ -538,15 +549,24 @@
805 def InstallBookmarksError(self, error):
806 """Problem installing the extension to sync bookmarks."""
807
808+ #---
809+
810+ @log_call(logger.info)
811+ @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
812+ def shutdown(self):
813+ """Shutdown this service."""
814+ self.backend.shutdown()
815+
816
817 def init_mainloop():
818 """Start the DBus mainloop."""
819 DBusGMainLoop(set_as_default=True)
820
821
822-def run_mainloop():
823+def run_mainloop(loop=None):
824 """Run the gobject main loop."""
825- loop = gobject.MainLoop()
826+ if loop is None:
827+ loop = gobject.MainLoop()
828 loop.run()
829
830
831@@ -566,10 +586,10 @@
832 return dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
833
834
835-def publish_backend(backend=None):
836+def publish_backend(backend=None, shutdown_func=None):
837 """Publish the backend on the DBus."""
838 if backend is None:
839- backend = ControlBackend()
840+ backend = ControlBackend(shutdown_func=shutdown_func)
841 return ControlPanelBackend(backend=backend,
842 object_path=DBUS_PREFERENCES_PATH,
843 bus_name=get_busname())
844@@ -579,7 +599,8 @@
845 """Hook the DBus listeners and start the main loop."""
846 init_mainloop()
847 if register_service():
848- publish_backend()
849- run_mainloop()
850+ loop = gobject.MainLoop()
851+ publish_backend(shutdown_func=loop.quit)
852+ run_mainloop(loop=loop)
853 else:
854 print "Control panel backend already running."
855
856=== modified file 'ubuntuone/controlpanel/gtk/__init__.py'
857--- ubuntuone/controlpanel/gtk/__init__.py 2011-03-23 16:06:41 +0000
858+++ ubuntuone/controlpanel/gtk/__init__.py 2011-04-08 19:43:13 +0000
859@@ -18,5 +18,7 @@
860
861 """The GTK graphical interface for the control panel for Ubuntu One."""
862
863-DBUS_BUS_NAME = "com.ubuntuone.controlpanel.gui"
864-TRANSLATION_DOMAIN = "ubuntuone-control-panel"
865+DBUS_BUS_NAME = 'com.ubuntuone.controlpanel.gui'
866+DBUS_PATH = '/gui'
867+DBUS_IFACE_GUI = 'com.ubuntuone.controlpanel.gui'
868+TRANSLATION_DOMAIN = 'ubuntuone-control-panel'
869
870=== modified file 'ubuntuone/controlpanel/gtk/gui.py'
871--- ubuntuone/controlpanel/gtk/gui.py 2011-03-23 16:06:41 +0000
872+++ ubuntuone/controlpanel/gtk/gui.py 2011-04-08 19:43:13 +0000
873@@ -1,6 +1,7 @@
874 # -*- coding: utf-8 -*-
875
876 # Authors: Natalia B Bidart <natalia.bidart@canonical.com>
877+# Eric Casteleijn <eric.casteleijn@canonical.com>
878 #
879 # Copyright 2010 Canonical Ltd.
880 #
881@@ -31,6 +32,7 @@
882 import gobject
883 import ubuntu_sso
884
885+from dbus.mainloop.glib import DBusGMainLoop
886 from ubuntu_sso import networkstate
887 from ubuntu_sso.credentials import (TC_URL_KEY, HELP_TEXT_KEY, WINDOW_ID_KEY,
888 PING_URL_KEY)
889@@ -41,6 +43,9 @@
890 PING_URL as U1_PING_URL, DESCRIPTION as U1_DESCRIPTION)
891 # pylint: enable=E0611,F0401
892
893+from ubuntuone.controlpanel.gtk import (
894+ DBUS_IFACE_GUI, DBUS_BUS_NAME as DBUS_BUS_NAME_GUI,
895+ DBUS_PATH as DBUS_PATH_GUI)
896 from ubuntuone.controlpanel.gtk.widgets import LabelLoading, PanelTitle
897 # Use ubiquity package when ready (LP: #673665)
898 from ubuntuone.controlpanel.gtk.widgets import GreyableBin
899@@ -50,10 +55,18 @@
900 from ubuntuone.controlpanel.backend import (DEVICE_TYPE_PHONE,
901 DEVICE_TYPE_COMPUTER, bool_str)
902 from ubuntuone.controlpanel.logger import setup_logging, log_call
903-from ubuntuone.controlpanel.utils import get_data_file
904+from ubuntuone.controlpanel.utils import (get_data_file,
905+ ERROR_TYPE, ERROR_MESSAGE)
906
907 from ubuntuone.controlpanel.gtk import package_manager, TRANSLATION_DOMAIN
908
909+try:
910+ from gi.repository import Unity # pylint: disable=E0611
911+ USE_LIBUNITY = True
912+ U1_DOTDESKTOP = "ubuntuone-control-panel-gtk.desktop"
913+except ImportError:
914+ USE_LIBUNITY = False
915+
916 logger = setup_logging('gtk.gui')
917 _ = gettext.gettext
918
919@@ -63,6 +76,7 @@
920 ERROR_COLOR = 'red'
921 LOADING = _('Loading...')
922 VALUE_ERROR = _('Value could not be retrieved.')
923+UNKNOWN_ERROR = _('Unknown error')
924 WARNING_MARKUP = '<span foreground="%s"><b>%%s</b></span>' % ERROR_COLOR
925 KILOBYTES = 1024
926 NO_OP = lambda *a, **kw: None
927@@ -74,6 +88,49 @@
928 logger.error('Error handler received: %r, %r', args, kwargs)
929
930
931+def register_service(bus):
932+ """Try to register DBus service for making sure we run only one instance.
933+
934+ Return True if succesfully registered, False if already running.
935+ """
936+ name = bus.request_name(DBUS_BUS_NAME_GUI,
937+ dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
938+ return name != dbus.bus.REQUEST_NAME_REPLY_EXISTS
939+
940+
941+def publish_service(window=None, switch_to='', alert=False):
942+ """Publish the service on DBus."""
943+ if window is None:
944+ window = ControlPanelWindow(switch_to=switch_to, alert=alert)
945+ return ControlPanelService(window)
946+
947+
948+def main(switch_to='', alert=False):
949+ """Hook the DBus listeners and start the main loop."""
950+ DBusGMainLoop(set_as_default=True)
951+ bus = dbus.SessionBus()
952+ if register_service(bus):
953+ publish_service(switch_to=switch_to, alert=alert)
954+ else:
955+ obj = bus.get_object(DBUS_BUS_NAME_GUI, DBUS_PATH_GUI)
956+ service = dbus.Interface(obj, dbus_interface=DBUS_IFACE_GUI)
957+
958+ def gui_error_handler(*args, **kwargs):
959+ """Log errors when calling D-Bus methods in a async way."""
960+ logger.error('Error handler received: %r, %r', args, kwargs)
961+ gtk.main_quit()
962+
963+ def gui_reply_handler(*args, **kwargs):
964+ """Exit when done."""
965+ gtk.main_quit()
966+
967+ service.switch_to_alert(
968+ switch_to, alert, reply_handler=gui_reply_handler,
969+ error_handler=gui_error_handler)
970+
971+ gtk.main()
972+
973+
974 def filter_by_app_name(f):
975 """Excecute 'f' filtering by app_name."""
976
977@@ -192,10 +249,19 @@
978 self.message.set_markup(message)
979
980 @log_call(logger.error)
981- def on_error(self, message=None):
982+ def on_error(self, message=None, error_dict=None):
983 """Use this callback to stop the Loading and set a warning message."""
984- if message == None:
985+ if message is None and error_dict is None:
986 message = VALUE_ERROR
987+ elif message is None and error_dict is not None:
988+ error_type = error_dict.get(ERROR_TYPE, UNKNOWN_ERROR)
989+ error_msg = error_dict.get(ERROR_MESSAGE)
990+ if error_msg:
991+ message = "%s (%s: %s)" % (VALUE_ERROR, error_type, error_msg)
992+ else:
993+ message = "%s (%s)" % (VALUE_ERROR, error_type)
994+
995+ assert message is not None
996
997 self.message.stop()
998 self.message.set_markup(WARNING_MARKUP % message)
999@@ -492,6 +558,10 @@
1000 elif name == self.MUSIC_DISPLAY_NAME:
1001 icon_name = self.MUSIC_ICON_NAME
1002
1003+ if volume[u'path'] is None:
1004+ logger.warning('on_volumes_info_ready: about to store a '
1005+ 'volume with None path: %r', volume)
1006+
1007 row = (name, bool(volume[u'subscribed']), icon_name, True,
1008 sensitive, gtk.ICON_SIZE_MENU, volume['volume_id'],
1009 volume[u'path'])
1010@@ -509,7 +579,7 @@
1011 @log_call(logger.error)
1012 def on_volumes_info_error(self, error_dict=None):
1013 """Backend notifies of an error when fetching volumes info."""
1014- self.on_error()
1015+ self.on_error(error_dict=error_dict)
1016
1017 @log_call(logger.info)
1018 def on_volume_settings_changed(self, volume_id):
1019@@ -548,7 +618,14 @@
1020 """The user double clicked on a row."""
1021 treeiter = self.volumes_store.get_iter(path)
1022 volume_path = self.volumes_store.get_value(treeiter, 7)
1023- uri_hook(None, FILE_URI_PREFIX + volume_path)
1024+ if volume_path is None:
1025+ logger.warning('on_volumes_view_row_activated: volume_path for '
1026+ 'tree_path %r is None', path)
1027+ elif not os.path.exists(volume_path):
1028+ logger.warning('on_volumes_view_row_activated: path %r '
1029+ 'does not exist', volume_path)
1030+ else:
1031+ uri_hook(None, FILE_URI_PREFIX + volume_path)
1032
1033 def load(self):
1034 """Load the volume list."""
1035@@ -827,7 +904,7 @@
1036 @log_call(logger.error)
1037 def on_devices_info_error(self, error_dict=None):
1038 """Backend notifies of an error when fetching volumes info."""
1039- self.on_error()
1040+ self.on_error(error_dict=error_dict)
1041 self.is_processing = False
1042
1043 @log_call(logger.warning)
1044@@ -992,7 +1069,8 @@
1045 def on_file_sync_status_changed(self, status):
1046 """File Sync status changed."""
1047 enabled = status != backend.FILE_SYNC_DISABLED
1048- logger.info('FileSyncService: enabled? %r', enabled)
1049+ logger.info('FileSyncService: on_file_sync_status_changed: '
1050+ 'status %r, enabled? %r', status, enabled)
1051 self.check_button.set_active(enabled)
1052 # if service is disabled, disable the action_button
1053 self.action_button.set_sensitive(enabled)
1054@@ -1025,8 +1103,8 @@
1055 class DesktopcouchService(Service):
1056 """A desktopcouch service."""
1057
1058- INSTALL_PACKAGE = _('Install the %(plugin_name)s '
1059- 'for %(service_name)s sync')
1060+ INSTALL_PACKAGE = _('Install the %(plugin_name)s for the sync service: '
1061+ '%(service_name)s')
1062
1063 def __init__(self, service_id, name, enabled,
1064 container, check_button,
1065@@ -1152,6 +1230,7 @@
1066 @log_call(logger.debug)
1067 def load(self):
1068 """Load info."""
1069+ self.replications.hide()
1070 if self.install_box is not None:
1071 self.itself.remove(self.install_box)
1072 self.install_box = None
1073@@ -1159,7 +1238,6 @@
1074 logger.info('load: has_desktopcouch? %r', self.has_desktopcouch)
1075 if not self.has_desktopcouch:
1076 self.message.set_text('')
1077- self.replications.hide()
1078
1079 self.install_box = InstallPackage(self.DESKTOPCOUCH_PKG)
1080 self.install_box.connect('finished', self.load_replications)
1081@@ -1211,7 +1289,7 @@
1082 error_dict.get('error_type', None) == 'NoPairingRecord':
1083 self.on_error(self.NO_PAIRING_RECORD)
1084 else:
1085- self.on_error()
1086+ self.on_error(error_dict=error_dict)
1087
1088
1089 class FileSyncStatus(gtk.HBox, ControlPanelMixin):
1090@@ -1252,6 +1330,8 @@
1091 self.button.connect('clicked', self._on_button_clicked)
1092 self.pack_start(self.button, expand=False)
1093
1094+ self.show_all()
1095+
1096 self.backend.connect_to_signal('FileSyncStatusDisabled',
1097 self.on_file_sync_status_disabled)
1098 self.backend.connect_to_signal('FileSyncStatusStarting',
1099@@ -1268,10 +1348,10 @@
1100 self.on_file_sync_status_error)
1101 self.backend.connect_to_signal('FilesStartError',
1102 self.on_files_start_error)
1103-
1104- self.backend.file_sync_status(reply_handler=NO_OP,
1105- error_handler=error_handler)
1106- self.show_all()
1107+ self.backend.connect_to_signal('FilesEnabled',
1108+ self.on_file_sync_status_starting)
1109+ self.backend.connect_to_signal('FilesDisabled',
1110+ self.on_file_sync_status_disabled)
1111
1112 def _update_status(self, msg, action, callback,
1113 icon=None, color=None, tooltip=None):
1114@@ -1296,42 +1376,42 @@
1115 button.get_data('callback')(button)
1116
1117 @log_call(logger.info)
1118- def on_file_sync_status_disabled(self, msg):
1119+ def on_file_sync_status_disabled(self, msg=None):
1120 """Backend notifies of file sync status being disabled."""
1121 self._update_status(self.FILE_SYNC_DISABLED,
1122 self.ENABLE, self.on_enable_clicked,
1123 '✘', 'red', self.ENABLE_TOOLTIP)
1124
1125 @log_call(logger.info)
1126- def on_file_sync_status_starting(self, msg):
1127+ def on_file_sync_status_starting(self, msg=None):
1128 """Backend notifies of file sync status being starting."""
1129 self._update_status(self.FILE_SYNC_STARTING,
1130 self.STOP, self.on_stop_clicked,
1131 '⇅', ORANGE, self.STOP_TOOLTIP)
1132
1133 @log_call(logger.info)
1134- def on_file_sync_status_stopped(self, msg):
1135+ def on_file_sync_status_stopped(self, msg=None):
1136 """Backend notifies of file sync being stopped."""
1137 self._update_status(self.FILE_SYNC_STOPPED,
1138 self.START, self.on_start_clicked,
1139 '✘', 'red', self.START_TOOLTIP)
1140
1141 @log_call(logger.info)
1142- def on_file_sync_status_disconnected(self, msg):
1143+ def on_file_sync_status_disconnected(self, msg=None):
1144 """Backend notifies of file sync status being ready."""
1145 self._update_status(self.FILE_SYNC_DISCONNECTED,
1146 self.CONNECT, self.on_connect_clicked,
1147 '✘', 'red', self.CONNECT_TOOLTIP,)
1148
1149 @log_call(logger.info)
1150- def on_file_sync_status_syncing(self, msg):
1151+ def on_file_sync_status_syncing(self, msg=None):
1152 """Backend notifies of file sync status being syncing."""
1153 self._update_status(self.FILE_SYNC_SYNCING,
1154 self.DISCONNECT, self.on_disconnect_clicked,
1155 '⇅', ORANGE, self.DISCONNECT_TOOLTIP)
1156
1157 @log_call(logger.info)
1158- def on_file_sync_status_idle(self, msg):
1159+ def on_file_sync_status_idle(self, msg=None):
1160 """Backend notifies of file sync status being idle."""
1161 self._update_status(self.FILE_SYNC_IDLE,
1162 self.DISCONNECT, self.on_disconnect_clicked,
1163@@ -1385,6 +1465,11 @@
1164 self.backend.stop_files(reply_handler=NO_OP,
1165 error_handler=error_handler)
1166
1167+ def load(self):
1168+ """Load the information."""
1169+ self.backend.file_sync_status(reply_handler=NO_OP,
1170+ error_handler=error_handler)
1171+
1172
1173 class ManagementPanel(gtk.VBox, ControlPanelMixin):
1174 """The management panel.
1175@@ -1396,6 +1481,7 @@
1176 __gsignals__ = {
1177 'local-device-removed': (gobject.SIGNAL_RUN_FIRST,
1178 gobject.TYPE_NONE, ()),
1179+ 'unauthorized': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
1180 }
1181
1182 QUOTA_LABEL = _('Using %(used)s of %(total)s (%(percentage).0f%%)')
1183@@ -1421,6 +1507,8 @@
1184 self.on_account_info_ready)
1185 self.backend.connect_to_signal('AccountInfoError',
1186 self.on_account_info_error)
1187+ self.backend.connect_to_signal('UnauthorizedError',
1188+ self.on_unauthorized_error)
1189
1190 self.quota_progressbar.set_sensitive(False)
1191
1192@@ -1461,7 +1549,11 @@
1193
1194 self.services_button.set_name(self.SERVICES_BUTTON_NAME)
1195 self.services_button.set_tooltip_text(self.SERVICES_BUTTON_TOOLTIP)
1196- self.services.load()
1197+
1198+ self.enable_volumes = lambda: self.volumes_button.set_sensitive(True)
1199+ self.disable_volumes = lambda: self.volumes_button.set_sensitive(False)
1200+ self.backend.connect_to_signal('FilesEnabled', self.enable_volumes)
1201+ self.backend.connect_to_signal('FilesDisabled', self.disable_volumes)
1202
1203 def _update_quota(self, msg, data=None):
1204 """Update the quota info."""
1205@@ -1491,6 +1583,8 @@
1206 """Load the account info and file sync status list."""
1207 self.backend.account_info(reply_handler=NO_OP,
1208 error_handler=error_handler)
1209+ self.status_label.load()
1210+ self.services.load()
1211
1212 @log_call(logger.debug)
1213 def on_account_info_ready(self, info):
1214@@ -1506,15 +1600,22 @@
1215 """Backend notifies of an error when fetching account info."""
1216 self._update_quota(msg='')
1217
1218-
1219-class ControlPanel(gtk.Notebook):
1220+ @log_call(logger.error)
1221+ def on_unauthorized_error(self, error_dict=None):
1222+ """Backend notifies that credentials are not valid."""
1223+ self.emit('unauthorized')
1224+
1225+
1226+class ControlPanel(gtk.Notebook, ControlPanelMixin):
1227 """The control panel per se, can be added into any other widget."""
1228
1229 # should not be any larger than 736x525
1230
1231 def __init__(self, main_window):
1232 gtk.Notebook.__init__(self)
1233+ ControlPanelMixin.__init__(self)
1234 gtk.link_button_set_uri_hook(uri_hook)
1235+ self.connect('destroy', self.shutdown)
1236
1237 self.main_window = main_window
1238
1239@@ -1531,6 +1632,8 @@
1240 self.on_show_management_panel)
1241 self.management.connect('local-device-removed',
1242 self.on_show_overview_panel)
1243+ self.management.connect('unauthorized',
1244+ self.on_show_overview_panel)
1245
1246 self.show()
1247 self.on_show_overview_panel()
1248@@ -1538,6 +1641,12 @@
1249 logger.debug('%s: started (window size %r).',
1250 self.__class__.__name__, self.get_size_request())
1251
1252+ def shutdown(self, *args, **kwargs):
1253+ """Shutdown backend."""
1254+ logger.info('Shutting down...')
1255+ self.backend.shutdown(reply_handler=NO_OP,
1256+ error_handler=error_handler)
1257+
1258 def on_show_overview_panel(self, widget=None):
1259 """Show the overview panel."""
1260 self.set_current_page(0)
1261@@ -1550,18 +1659,42 @@
1262 if credentials_are_new:
1263 # redirect user to services page to start using Ubuntu One
1264 self.management.services_button.clicked()
1265+ # instruct syncdaemon to connect
1266+ self.backend.connect_files(reply_handler=NO_OP,
1267+ error_handler=error_handler)
1268
1269 self.next_page()
1270
1271
1272+class ControlPanelService(dbus.service.Object):
1273+ """DBUS service that exposes some of the window's methods."""
1274+
1275+ def __init__(self, window):
1276+ self.window = window
1277+ bus_name = dbus.service.BusName(
1278+ DBUS_BUS_NAME_GUI, bus=dbus.SessionBus())
1279+ dbus.service.Object.__init__(
1280+ self, bus_name=bus_name, object_path=DBUS_PATH_GUI)
1281+
1282+ @log_call(logger.debug)
1283+ @dbus.service.method(dbus_interface=DBUS_IFACE_GUI, in_signature='sb')
1284+ def switch_to_alert(self, panel='', alert=False):
1285+ """Switch to named panel."""
1286+ if panel:
1287+ self.window.switch_to(panel)
1288+ if alert:
1289+ self.window.draw_attention()
1290+
1291+
1292 class ControlPanelWindow(gtk.Window):
1293 """The main window for the Ubuntu One control panel."""
1294
1295 TITLE = _('%(app_name)s Control Panel')
1296
1297- def __init__(self, switch_to=None, alert=False):
1298+ def __init__(self, switch_to='', alert=False):
1299 super(ControlPanelWindow, self).__init__()
1300
1301+ self.connect('focus-in-event', self.remove_urgency)
1302 self.set_title(self.TITLE % {'app_name': U1_APP_NAME})
1303 self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
1304 self.set_icon_name('ubuntuone')
1305@@ -1569,9 +1702,7 @@
1306
1307 self.connect('delete-event', lambda w, e: gtk.main_quit())
1308 if alert:
1309- # NOTE this should prevent focus stealing but it does not :(
1310- self.present_with_time(1)
1311- self.set_urgency_hint(True)
1312+ self.draw_attention()
1313 else:
1314 self.present()
1315
1316@@ -1580,17 +1711,35 @@
1317
1318 logger.info('Starting %s pointing at panel: %r.',
1319 self.__class__.__name__, switch_to)
1320- if switch_to is not None:
1321- button = getattr(self.control_panel.management,
1322- '%s_button' % switch_to, None)
1323- if button is not None:
1324- button.clicked()
1325- else:
1326- logger.warning('Could not start at panel: %r.', switch_to)
1327+ if switch_to:
1328+ self.switch_to(switch_to)
1329
1330 logger.debug('%s: started (window size %r).',
1331 self.__class__.__name__, self.get_size_request())
1332
1333+ def remove_urgency(self, *args, **kwargs):
1334+ """Remove urgency from the launcher entry."""
1335+ if not USE_LIBUNITY:
1336+ return
1337+ entry = Unity.LauncherEntry.get_for_desktop_id(U1_DOTDESKTOP)
1338+ if entry.props.urgent:
1339+ self.switch_to('volumes')
1340+ entry.props.urgent = False
1341+
1342+ def draw_attention(self):
1343+ """Draw attention to the control panel."""
1344+ self.present_with_time(1)
1345+ self.set_urgency_hint(True)
1346+
1347+ def switch_to(self, panel):
1348+ """Switch to named panel."""
1349+ button = getattr(
1350+ self.control_panel.management, '%s_button' % panel, None)
1351+ if button is not None:
1352+ button.clicked()
1353+ else:
1354+ logger.warning('Could not start at panel: %r.', panel)
1355+
1356 def main(self):
1357 """Run the main loop of the widget toolkit."""
1358 logger.debug('Starting GTK main loop.')
1359
1360=== modified file 'ubuntuone/controlpanel/gtk/package_manager.py'
1361--- ubuntuone/controlpanel/gtk/package_manager.py 2011-02-23 13:57:42 +0000
1362+++ ubuntuone/controlpanel/gtk/package_manager.py 2011-04-08 19:43:13 +0000
1363@@ -20,13 +20,14 @@
1364
1365 import apt
1366 import aptdaemon.client
1367+# pylint: disable=W0404
1368 import aptdaemon.enums
1369
1370 try:
1371- # Unable to import 'defer', pylint: disable=F0401,E0611
1372+ # Unable to import 'defer', pylint: disable=F0401,E0611,W0404
1373 from aptdaemon.defer import inline_callbacks, return_value
1374 except ImportError:
1375- # Unable to import 'defer', pylint: disable=F0401,E0611
1376+ # Unable to import 'defer', pylint: disable=F0401,E0611,W0404
1377 from defer import inline_callbacks, return_value
1378 from aptdaemon.gtkwidgets import AptProgressBar
1379
1380
1381=== modified file 'ubuntuone/controlpanel/gtk/tests/__init__.py'
1382--- ubuntuone/controlpanel/gtk/tests/__init__.py 2011-03-10 02:59:44 +0000
1383+++ ubuntuone/controlpanel/gtk/tests/__init__.py 2011-04-08 19:43:13 +0000
1384@@ -180,10 +180,19 @@
1385 'replications_info', 'change_replication_settings', # replications
1386 'file_sync_status', 'enable_files', 'disable_files', # files
1387 'connect_files', 'disconnect_files',
1388- 'restart_files', 'start_files', 'stop_files',
1389+ 'restart_files', 'start_files', 'stop_files', 'shutdown',
1390 ]
1391
1392
1393+class FakeControlPanelBackend(FakedDBusBackend):
1394+ """Fake a Control Panel Service, act as a dbus.Interface."""
1395+
1396+ bus_name = gui.DBUS_BUS_NAME_GUI
1397+ object_path = gui.DBUS_PATH_GUI
1398+ iface = gui.DBUS_IFACE_GUI
1399+ exposed_methods = ['draw_attention', 'switch_to']
1400+
1401+
1402 class FakedSessionBus(object):
1403 """Fake a session bus."""
1404
1405@@ -202,6 +211,9 @@
1406 *args, **kwargs)
1407 if dbus_interface == gui.ubuntu_sso.DBUS_CREDENTIALS_IFACE:
1408 return FakedSSOBackend(obj, dbus_interface, *args, **kwargs)
1409+ if dbus_interface == gui.DBUS_IFACE_GUI:
1410+ return FakeControlPanelBackend(
1411+ obj, dbus_interface, *args, **kwargs)
1412
1413
1414 class FakedPackageManager(object):
1415
1416=== modified file 'ubuntuone/controlpanel/gtk/tests/test_gui.py'
1417--- ubuntuone/controlpanel/gtk/tests/test_gui.py 2011-03-10 02:59:44 +0000
1418+++ ubuntuone/controlpanel/gtk/tests/test_gui.py 2011-04-08 19:43:13 +0000
1419@@ -278,6 +278,7 @@
1420
1421 def test_clicking_on_row_opens_folder(self):
1422 """The folder activated is opened."""
1423+ self.patch(gui.os.path, 'exists', lambda *a: True)
1424 self.patch(gui, 'uri_hook', self._set_called)
1425 self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
1426
1427@@ -287,6 +288,33 @@
1428 self.assertEqual(self._called,
1429 ((None, gui.FILE_URI_PREFIX + ROOT['path']), {}))
1430
1431+ def test_clicking_on_row_handles_path_none(self):
1432+ """None paths are properly handled."""
1433+ self.patch(gui, 'uri_hook', self._set_called)
1434+ self.patch(self.ui.volumes_store, 'get_value', lambda *a: None)
1435+ self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
1436+
1437+ self.ui.volumes_view.row_activated('0:0',
1438+ self.ui.volumes_view.get_column(0))
1439+
1440+ self.assertTrue(self.memento.check_warning('tree_path (0, 0)',
1441+ 'volume_path', 'is None'))
1442+ self.assertEqual(self._called, False)
1443+
1444+ def test_clicking_on_row_handles_path_non_existent(self):
1445+ """Not-existent paths are properly handled."""
1446+ self.patch(gui.os.path, 'exists', lambda *a: False)
1447+ self.patch(gui, 'uri_hook', self._set_called)
1448+ path = 'not-in-disk'
1449+ self.patch(self.ui.volumes_store, 'get_value', lambda *a: path)
1450+ self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
1451+
1452+ self.ui.volumes_view.row_activated('0:0',
1453+ self.ui.volumes_view.get_column(0))
1454+
1455+ self.assertTrue(self.memento.check_warning(path, 'does not exist'))
1456+ self.assertEqual(self._called, False)
1457+
1458 def test_on_volumes_info_ready_with_music_folder(self):
1459 """The volumes info is processed when ready."""
1460 info = [(u'', u'147852369', [ROOT] + [MUSIC_FOLDER])]
1461@@ -1646,12 +1674,15 @@
1462 ('FileSyncStatusIdle', [self.ui.on_file_sync_status_idle]),
1463 ('FileSyncStatusError', [self.ui.on_file_sync_status_error]),
1464 ('FilesStartError', [self.ui.on_files_start_error]),
1465+ ('FilesDisabled', [self.ui.on_file_sync_status_disabled]),
1466+ ('FilesEnabled', [self.ui.on_file_sync_status_starting]),
1467 )
1468 for sig, handlers in matches:
1469 self.assertEqual(self.ui.backend._signals[sig], handlers)
1470
1471- def test_file_sync_status_is_requested(self):
1472+ def test_file_sync_status_is_requested_on_load(self):
1473 """The file sync status is requested to the backend."""
1474+ self.ui.load()
1475 self.assert_backend_called('file_sync_status', ())
1476
1477 def test_on_file_sync_status_disabled(self):
1478@@ -1882,11 +1913,32 @@
1479 self.assertEqual(self.ui.backend._signals['AccountInfoError'],
1480 [self.ui.on_account_info_error])
1481
1482+ def test_backend_unauthorized_signal(self):
1483+ """The proper signals are connected to the backend."""
1484+ self.assertEqual(self.ui.backend._signals['UnauthorizedError'],
1485+ [self.ui.on_unauthorized_error])
1486+
1487+ def test_no_backend_calls_before_load(self):
1488+ """No calls are made to the backend before load() is called."""
1489+ self.assertEqual(self.ui.backend._called, {})
1490+
1491 def test_account_info_is_requested_on_load(self):
1492 """The account info is requested to the backend."""
1493 self.ui.load()
1494 self.assert_backend_called('account_info', ())
1495
1496+ def test_file_sync_status_info_is_requested_on_load(self):
1497+ """The file sync status info is requested to the backend."""
1498+ self.patch(self.ui.status_label, 'load', self._set_called)
1499+ self.ui.load()
1500+ self.assertEqual(self._called, ((), {}))
1501+
1502+ def test_replications_info_is_requested_on_load(self):
1503+ """The replications info is requested to the backend."""
1504+ self.patch(self.ui.services, 'load', self._set_called)
1505+ self.ui.load()
1506+ self.assertEqual(self._called, ((), {}))
1507+
1508 def test_dashboard_panel_is_packed(self):
1509 """The dashboard panel is packed."""
1510 self.assertIsInstance(self.ui.dashboard, gui.DashboardPanel)
1511@@ -1999,6 +2051,23 @@
1512 self.assertIsInstance(self.ui.status_label, gui.FileSyncStatus)
1513 self.assertIn(self.ui.status_label, self.ui.status_box.get_children())
1514
1515+ def test_backend_file_sync_signals(self):
1516+ """The proper signals are connected to the backend."""
1517+ self.assertEqual(self.ui.backend._signals['FilesEnabled'],
1518+ [self.ui.enable_volumes])
1519+ self.assertEqual(self.ui.backend._signals['FilesDisabled'],
1520+ [self.ui.disable_volumes])
1521+
1522+ def test_enable_volumes(self):
1523+ """The volumes tab is properly enabled."""
1524+ self.ui.enable_volumes()
1525+ self.assertTrue(self.ui.volumes_button.get_sensitive())
1526+
1527+ def test_disable_volumes(self):
1528+ """The volumes tab is properly disabled."""
1529+ self.ui.disable_volumes()
1530+ self.assertFalse(self.ui.volumes_button.get_sensitive())
1531+
1532 def test_local_device_removed_is_emitted(self):
1533 """Signal local-device-removed is sent when DevicesPanel emits it."""
1534 self.ui.connect('local-device-removed', self._set_called)
1535@@ -2024,3 +2093,9 @@
1536 actual = getattr(self.ui, '%s_button' % tab).get_tooltip_text()
1537 expected = getattr(self.ui, '%s_BUTTON_TOOLTIP' % tab.upper())
1538 self.assertEqual(actual, expected)
1539+
1540+ def test_on_unauthorized_error(self):
1541+ """On invalid credentials, proper signal is sent."""
1542+ self.ui.connect('unauthorized', self._set_called)
1543+ self.ui.on_unauthorized_error()
1544+ self.assertEqual(self._called, ((self.ui,), {}))
1545
1546=== modified file 'ubuntuone/controlpanel/gtk/tests/test_gui_basic.py'
1547--- ubuntuone/controlpanel/gtk/tests/test_gui_basic.py 2011-03-23 16:06:41 +0000
1548+++ ubuntuone/controlpanel/gtk/tests/test_gui_basic.py 2011-04-08 19:43:13 +0000
1549@@ -29,6 +29,43 @@
1550 # pylint: disable=W0201, W0212
1551
1552
1553+class FakeLauncherEntryProps(object):
1554+ """A fake Unity.LauncherEntry.props"""
1555+
1556+ urgent = True
1557+
1558+
1559+THE_FLEP = FakeLauncherEntryProps()
1560+
1561+
1562+class FakeLauncherEntry(object):
1563+ """A fake Unity.LauncherEntry"""
1564+
1565+ def __init__(self):
1566+ """Initialize this fake instance."""
1567+ self.props = THE_FLEP
1568+
1569+ @staticmethod
1570+ def get_for_desktop_id(dotdesktop):
1571+ """Find the LauncherEntry for a given dotdesktop."""
1572+ return FakeLauncherEntry()
1573+
1574+
1575+class FakeControlPanelService(object):
1576+ """Fake service."""
1577+ def __init__(self, window):
1578+ self.window = window
1579+ self.called = []
1580+
1581+ def draw_attention(self):
1582+ """Draw attention to the control panel."""
1583+ self.called.append('draw_attention')
1584+
1585+ def switch_to(self, panel):
1586+ """Switch to named panel."""
1587+ self.called.append(('switch_to', panel))
1588+
1589+
1590 class ControlPanelMixinTestCase(BaseTestCase):
1591 """The test suite for the control panel widget."""
1592
1593@@ -49,25 +86,30 @@
1594
1595 klass = gui.ControlPanelWindow
1596
1597+ def setUp(self):
1598+ self.patch(gui, 'ControlPanelService', FakeControlPanelService)
1599+ super(ControlPanelWindowTestCase, self).setUp()
1600+
1601 def test_is_a_window(self):
1602 """Inherits from gtk.Window."""
1603 self.assertIsInstance(self.ui, gui.gtk.Window)
1604
1605 def test_startup_visibility(self):
1606 """The widget is visible at startup."""
1607- self.assertTrue(self.ui.get_visible(), 'must be visible at startup.')
1608+ self.assertTrue(self.ui.get_visible(), 'was not visible at startup.')
1609
1610 def test_main_start_gtk_main_loop(self):
1611 """The GTK main loop is started when calling main()."""
1612 self.patch(gui.gtk, 'main', self._set_called)
1613 self.ui.main()
1614- self.assertEqual(self._called, ((), {}), 'gtk.main was called.')
1615+ self.assertEqual(self._called, ((), {}), 'gtk.main was not called.')
1616
1617 def test_closing_stops_the_main_lopp(self):
1618 """The GTK main loop is stopped when closing the window."""
1619 self.patch(gui.gtk, 'main_quit', self._set_called)
1620 self.ui.emit('delete-event', None)
1621- self.assertEqual(self._called, ((), {}), 'gtk.main_quit was called.')
1622+ self.assertEqual(
1623+ self._called, ((), {}), 'gtk.main_quit was not called.')
1624
1625 def test_title_is_correct(self):
1626 """The window title is correct."""
1627@@ -96,6 +138,32 @@
1628 """Max size is not bigger than 736x525 (LP: #645526, LP: #683164)."""
1629 self.assertTrue(self.ui.get_size_request() <= (736, 525))
1630
1631+ def test_focus_handler(self):
1632+ """When the window receives focus, the handler is called."""
1633+ THE_FLEP.urgent = True
1634+ self.patch(gui.Unity, "LauncherEntry", FakeLauncherEntry)
1635+ cp = gui.ControlPanelWindow()
1636+ cp.emit('focus-in-event', None)
1637+ self.assertEqual(
1638+ False, THE_FLEP.urgent, 'remove_urgency should have been called.')
1639+
1640+
1641+class ControlPanelWindowAlertParamTestCase(BaseTestCase):
1642+ """The test suite for the control panel window when passing params."""
1643+
1644+ klass = gui.ControlPanelWindow
1645+ kwargs = {'alert': True}
1646+
1647+ def setUp(self):
1648+ self.patch(gui, 'ControlPanelService', FakeControlPanelService)
1649+ self.patch(gui.ControlPanelWindow, 'draw_attention', self._set_called)
1650+ super(ControlPanelWindowAlertParamTestCase, self).setUp()
1651+
1652+ def test_alert(self):
1653+ """Can pass a 'alert' parameter to draw attention to the window."""
1654+ self.assertEqual(
1655+ ((), {}), self._called, 'draw_attention should have been called.')
1656+
1657
1658 class ControlPanelWindowParamsTestCase(ControlPanelWindowTestCase):
1659 """The test suite for the control panel window when passing params."""
1660@@ -169,8 +237,10 @@
1661
1662 def test_on_show_management_panel(self):
1663 """A ManagementPanel is shown when the callback is executed."""
1664+ self.patch(self.ui.management, 'load', self._set_called)
1665 self.ui.on_show_management_panel()
1666 self.assert_current_tab_correct(self.ui.management)
1667+ self.assertEqual(self._called, ((), {}))
1668
1669 def test_on_show_management_panel_is_idempotent(self):
1670 """Only one ManagementPanel is shown."""
1671@@ -179,7 +249,7 @@
1672
1673 self.assert_current_tab_correct(self.ui.management)
1674
1675- def test_credentials_found_shows_dashboard_management_panel(self):
1676+ def test_credentials_found_shows_dashboard_panel(self):
1677 """On 'credentials-found' signal, the management panel is shown.
1678
1679 If first signal parameter is False, visible tab should be dashboard.
1680@@ -193,7 +263,7 @@
1681 self.ui.management.DASHBOARD_PAGE)
1682 self.assertEqual(self._called, ((), {}))
1683
1684- def test_credentials_found_shows_volumes_management_panel(self):
1685+ def test_credentials_found_shows_services_panel(self):
1686 """On 'credentials-found' signal, the management panel is shown.
1687
1688 If first signal parameter is True, visible tab should be services.
1689@@ -206,6 +276,12 @@
1690 self.assertEqual(self.ui.management.notebook.get_current_page(),
1691 self.ui.management.SERVICES_PAGE)
1692
1693+ def test_credentials_found_connects_syncdaemon(self):
1694+ """On 'credentials-found' signal, ask syncdaemon to connect."""
1695+ # credentials are new
1696+ self.ui.overview.emit('credentials-found', True, object())
1697+ self.assert_backend_called('connect_files', ())
1698+
1699 def test_local_device_removed_shows_overview_panel(self):
1700 """On 'local-device-removed' signal, the overview panel is shown."""
1701 self.ui.overview.emit('credentials-found', True, object())
1702@@ -213,6 +289,18 @@
1703
1704 self.assert_current_tab_correct(self.ui.overview)
1705
1706+ def test_unauthorized_shows_overview_panel(self):
1707+ """On 'unauthorized' signal, the overview panel is shown."""
1708+ self.ui.overview.emit('credentials-found', True, object())
1709+ self.ui.management.emit('unauthorized')
1710+
1711+ self.assert_current_tab_correct(self.ui.overview)
1712+
1713+ def test_backend_is_shutdown_on_close(self):
1714+ """When the control panel is closed, the backend is shutdown."""
1715+ self.ui.emit('destroy')
1716+ self.assert_backend_called('shutdown', ())
1717+
1718
1719 class UbuntuOneBinTestCase(BaseTestCase):
1720 """The test suite for a Ubuntu One panel."""
1721@@ -276,6 +364,42 @@
1722 self.assert_warning_correct(self.ui.message, msg)
1723 self.assertFalse(self.ui.message.active)
1724
1725+ def test_on_error_with_error_dict(self):
1726+ """Callback to stop the Loading and show the error from error_dict."""
1727+ msg = u'Qué mala suerte! <i>this does not rock</i>'
1728+ error_dict = {gui.ERROR_TYPE: 'YaddaError', gui.ERROR_MESSAGE: msg}
1729+ self.ui.on_error(error_dict=error_dict)
1730+
1731+ expected_msg = "%s (%s: %s)" % (gui.VALUE_ERROR, 'YaddaError', msg)
1732+ self.assert_warning_correct(self.ui.message, expected_msg)
1733+ self.assertFalse(self.ui.message.active)
1734+
1735+ def test_on_error_with_error_dict_without_error_type(self):
1736+ """Callback to stop the Loading and show the error from error_dict."""
1737+ error_dict = {}
1738+ self.ui.on_error(error_dict=error_dict)
1739+
1740+ expected_msg = "%s (%s)" % (gui.VALUE_ERROR, gui.UNKNOWN_ERROR)
1741+ self.assert_warning_correct(self.ui.message, expected_msg)
1742+ self.assertFalse(self.ui.message.active)
1743+
1744+ def test_on_error_with_error_dict_without_error_message(self):
1745+ """Callback to stop the Loading and show the error from error_dict."""
1746+ error_dict = {gui.ERROR_TYPE: 'YaddaError'}
1747+ self.ui.on_error(error_dict=error_dict)
1748+
1749+ expected_msg = "%s (%s)" % (gui.VALUE_ERROR, 'YaddaError')
1750+ self.assert_warning_correct(self.ui.message, expected_msg)
1751+ self.assertFalse(self.ui.message.active)
1752+
1753+ def test_on_error_with_message_and_error_dict(self):
1754+ """Callback to stop the Loading and show a info message."""
1755+ error_dict = {gui.ERROR_TYPE: 'YaddaError', gui.ERROR_MESSAGE: 'test'}
1756+ msg = 'WOW! <i>this does not rock</i> :-/'
1757+ self.ui.on_error(message=msg, error_dict=error_dict)
1758+ self.assert_warning_correct(self.ui.message, msg)
1759+ self.assertFalse(self.ui.message.active)
1760+
1761 def test_is_processing(self):
1762 """The flag 'is_processing' is False on start."""
1763 self.assertFalse(self.ui.is_processing)
1764
1765=== modified file 'ubuntuone/controlpanel/gtk/tests/test_package_manager.py'
1766--- ubuntuone/controlpanel/gtk/tests/test_package_manager.py 2011-01-07 20:07:39 +0000
1767+++ ubuntuone/controlpanel/gtk/tests/test_package_manager.py 2011-04-08 19:43:13 +0000
1768@@ -21,10 +21,10 @@
1769 import collections
1770
1771 try:
1772- # Unable to import 'defer', pylint: disable=F0401,E0611
1773+ # Unable to import 'defer', pylint: disable=F0401,E0611,W0404
1774 from aptdaemon import defer
1775 except ImportError:
1776- # Unable to import 'defer', pylint: disable=F0401,E0611
1777+ # Unable to import 'defer', pylint: disable=F0401,E0611,W0404
1778 import defer
1779
1780 from ubuntuone.controlpanel.gtk import package_manager
1781
1782=== modified file 'ubuntuone/controlpanel/integrationtests/__init__.py'
1783--- ubuntuone/controlpanel/integrationtests/__init__.py 2010-12-22 13:33:25 +0000
1784+++ ubuntuone/controlpanel/integrationtests/__init__.py 2011-04-08 19:43:13 +0000
1785@@ -20,7 +20,6 @@
1786 """Integration tests for the Ubuntu One Control Panel."""
1787
1788 import dbus
1789-import dbus.service
1790
1791 from ubuntuone.devtools.testcase import DBusTestCase as TestCase
1792
1793@@ -31,7 +30,7 @@
1794 """A DBus exception to be used in tests."""
1795
1796
1797-class MockDBusNoMethods(dbus.service.Object):
1798+class MockDBusNoMethods(dbus_service.dbus.service.Object):
1799 """A mock that fails at the DBus layer (because it's got no methods!)."""
1800
1801
1802
1803=== modified file 'ubuntuone/controlpanel/integrationtests/test_dbus_service.py'
1804--- ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2011-02-23 13:57:42 +0000
1805+++ ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2011-04-08 19:43:13 +0000
1806@@ -84,12 +84,23 @@
1807 rs = self.mocker.replace(rs_name)
1808 rs()
1809 self.mocker.result(True)
1810+
1811+ mainloop = "ubuntuone.controlpanel.dbus_service.gobject.MainLoop"
1812+ mainloop = self.mocker.replace(mainloop)
1813+ mainloop()
1814+ loop = self.mocker.mock()
1815+ self.mocker.result(loop)
1816+
1817+ shutdown_func = self.mocker.mock()
1818+ loop.quit # pylint: disable=W0104
1819+ self.mocker.result(shutdown_func)
1820+
1821 rml_name = "ubuntuone.controlpanel.dbus_service.run_mainloop"
1822 rml = self.mocker.replace(rml_name)
1823- rml()
1824+ rml(loop=loop)
1825 pb_name = "ubuntuone.controlpanel.dbus_service.publish_backend"
1826 pb = self.mocker.replace(pb_name)
1827- pb()
1828+ pb(shutdown_func=shutdown_func)
1829 self.mocker.replay()
1830 dbus_service.main()
1831
1832@@ -112,6 +123,10 @@
1833 dbus_service.STATUS_KEY: dbus_service.FILE_SYNC_IDLE,
1834 }
1835 status_changed_handler = None
1836+ shutdown_func = None
1837+
1838+ def __init__(self, shutdown_func=None):
1839+ MockBackend.shutdown_func = shutdown_func
1840
1841 def _process(self, result):
1842 """Process the request with the given result."""
1843@@ -197,6 +212,10 @@
1844 """Install the extension to sync bookmarks."""
1845 return self._process(None)
1846
1847+ def shutdown(self):
1848+ """Stop this service."""
1849+ self.shutdown_func()
1850+
1851
1852 class DBusServiceTestCase(TestCase):
1853 """Test for the DBus service."""
1854@@ -289,7 +308,8 @@
1855 def setUp(self):
1856 super(BaseTestCase, self).setUp()
1857 dbus_service.init_mainloop()
1858- be = dbus_service.publish_backend(MockBackend())
1859+ self.patch(dbus_service, 'ControlBackend', MockBackend)
1860+ be = dbus_service.publish_backend()
1861 self.addCleanup(be.remove_from_connection)
1862 bus = dbus.SessionBus()
1863 obj = bus.get_object(bus_name=DBUS_BUS_NAME,
1864@@ -604,6 +624,35 @@
1865 error_sig, success_sig, got_error_signal, method, *args)
1866
1867
1868+class OperationsAuthErrorTestCase(OperationsTestCase):
1869+ """Test for the DBus service operations when UnauthorizedError happens."""
1870+
1871+ def setUp(self):
1872+ super(OperationsAuthErrorTestCase, self).setUp()
1873+ self.patch(MockBackend, 'exception',
1874+ dbus_service.UnauthorizedError)
1875+
1876+ def assert_correct_method_call(self, success_sig, error_sig, success_cb,
1877+ method, *args):
1878+ """Call parent instance expecting UnauthorizedError signal."""
1879+
1880+ def inner_success_cb(*a):
1881+ """The success signal was received."""
1882+ if len(a) == 1:
1883+ error_dict = a[0]
1884+ else:
1885+ an_id, error_dict = a
1886+ self.assertEqual(an_id, args[0])
1887+
1888+ self.assertEqual(error_dict[dbus_service.ERROR_TYPE],
1889+ 'UnauthorizedError')
1890+ self.deferred.callback('success')
1891+
1892+ parent = super(OperationsAuthErrorTestCase, self)
1893+ return parent.assert_correct_method_call(
1894+ "UnauthorizedError", error_sig, inner_success_cb, method, *args)
1895+
1896+
1897 class FileSyncTestCase(BaseTestCase):
1898 """Test for the DBus service when requesting file sync status."""
1899
1900@@ -691,3 +740,18 @@
1901 cpbe = dbus_service.ControlPanelBackend(backend=be)
1902
1903 self.assertEqual(be.status_changed_handler, cpbe.process_status)
1904+
1905+
1906+class ShutdownTestCase(BaseTestCase):
1907+ """Test for the DBus service shurdown."""
1908+
1909+ @defer.inlineCallbacks
1910+ def test_shutdown(self):
1911+ """The service can be shutdown."""
1912+ called = []
1913+ MockBackend.shutdown_func = lambda *a: called.append('shutdown')
1914+ self.backend.shutdown(reply_handler=lambda: self.deferred.callback(1),
1915+ error_handler=self.got_error)
1916+ yield self.deferred
1917+
1918+ self.assertEqual(called, ['shutdown'])
1919
1920=== added file 'ubuntuone/controlpanel/integrationtests/test_gui_service.py'
1921--- ubuntuone/controlpanel/integrationtests/test_gui_service.py 1970-01-01 00:00:00 +0000
1922+++ ubuntuone/controlpanel/integrationtests/test_gui_service.py 2011-04-08 19:43:13 +0000
1923@@ -0,0 +1,98 @@
1924+# -*- coding: utf-8 -*-
1925+
1926+# Authors: Alejandro J. Cura <alecu@canonical.com>
1927+# Natalia B. Bidart <nataliabidart@canonical.com>
1928+# Eric Casteleijn <eric.casteleijn@canonical.com>
1929+#
1930+# Copyright 2011 Canonical Ltd.
1931+#
1932+# This program is free software: you can redistribute it and/or modify it
1933+# under the terms of the GNU General Public License version 3, as published
1934+# by the Free Software Foundation.
1935+#
1936+# This program is distributed in the hope that it will be useful, but
1937+# WITHOUT ANY WARRANTY; without even the implied warranties of
1938+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1939+# PURPOSE. See the GNU General Public License for more details.
1940+#
1941+# You should have received a copy of the GNU General Public License along
1942+# with this program. If not, see <http://www.gnu.org/licenses/>.
1943+
1944+"""Tests for the control panel backend DBus service."""
1945+
1946+import dbus
1947+import mocker
1948+
1949+from dbus.mainloop.glib import DBusGMainLoop
1950+
1951+from ubuntuone.controlpanel.gtk import gui
1952+from ubuntuone.devtools.testcase import DBusTestCase
1953+from twisted.trial.unittest import TestCase
1954+
1955+
1956+class MockWindow(object):
1957+ """A mock backend."""
1958+
1959+ exception = None
1960+
1961+ def __init__(self, switch_to=None, alert=False):
1962+ self.called = []
1963+
1964+ def draw_attention(self):
1965+ """Draw attention to the control panel."""
1966+ self.called.append('draw_attention')
1967+
1968+ def switch_to(self, panel):
1969+ """Switch to named panel."""
1970+ self.called.append(('switch_to', panel))
1971+
1972+
1973+class DBusServiceMockTestCase(TestCase):
1974+ """Tests for the main function."""
1975+
1976+ def setUp(self):
1977+ self.mocker = mocker.Mocker()
1978+
1979+ def tearDown(self):
1980+ self.mocker.restore()
1981+ self.mocker.verify()
1982+
1983+ def test_dbus_service_main(self):
1984+ """The main method starts the loop and hooks up to DBus."""
1985+ self.patch(gui, 'ControlPanelWindow', MockWindow)
1986+ dbus_gmain_loop = self.mocker.replace(
1987+ "dbus.mainloop.glib.DBusGMainLoop")
1988+ register_service = self.mocker.replace(
1989+ "ubuntuone.controlpanel.gtk.gui.register_service")
1990+ publish_service = self.mocker.replace(
1991+ "ubuntuone.controlpanel.gtk.gui.publish_service")
1992+ main = self.mocker.replace("gtk.main")
1993+ dbus_gmain_loop(set_as_default=True)
1994+ loop = self.mocker.mock()
1995+ self.mocker.result(loop)
1996+ register_service(mocker.ANY)
1997+ self.mocker.result(True)
1998+ publish_service(switch_to='', alert=False)
1999+ main()
2000+ self.mocker.replay()
2001+ gui.main()
2002+
2003+
2004+class DBusServiceTestCase(DBusTestCase):
2005+ """Test for the DBus service."""
2006+
2007+ def _set_called(self, *args, **kwargs):
2008+ """Keep track of function calls, useful for monkeypatching."""
2009+ self._called = (args, kwargs)
2010+
2011+ def setUp(self):
2012+ """Initialize each test run."""
2013+ super(DBusServiceTestCase, self).setUp()
2014+ DBusGMainLoop(set_as_default=True)
2015+ self._called = False
2016+
2017+ def test_register_service(self):
2018+ """The DBus service is successfully registered."""
2019+ bus = dbus.SessionBus()
2020+ ret = gui.register_service(bus)
2021+ self.assertTrue(ret)
2022
2023=== modified file 'ubuntuone/controlpanel/integrationtests/test_webclient.py'
2024--- ubuntuone/controlpanel/integrationtests/test_webclient.py 2010-12-06 12:27:11 +0000
2025+++ ubuntuone/controlpanel/integrationtests/test_webclient.py 2011-04-08 19:43:13 +0000
2026@@ -73,6 +73,10 @@
2027 devices_resource.contents = SAMPLE_RESOURCE
2028 root.putChild("devices", devices_resource)
2029 root.putChild("throwerror", resource.NoResource())
2030+ unauthorized = resource.ErrorPage(resource.http.UNAUTHORIZED,
2031+ "Unauthrorized", "Unauthrorized")
2032+ root.putChild("unauthorized", unauthorized)
2033+
2034 site = server.Site(root)
2035 application = service.Application('web')
2036 self.service_collection = service.IServiceCollection(application)
2037@@ -96,7 +100,7 @@
2038 class WebClientTestCase(TestCase):
2039 """Test for the webservice client."""
2040
2041- timeout = 5
2042+ timeout = 8
2043
2044 def setUp(self):
2045 super(WebClientTestCase, self).setUp()
2046@@ -123,6 +127,12 @@
2047 yield self.assertFailure(self.wc.call_api("throwerror"),
2048 webclient.WebClientError)
2049
2050+ @inlineCallbacks
2051+ def test_unauthorized(self):
2052+ """Detect when a request failed with UNAUTHORIZED."""
2053+ yield self.assertFailure(self.wc.call_api("unauthorized"),
2054+ webclient.UnauthorizedError)
2055+
2056
2057 class OAuthTestCase(TestCase):
2058 """Test for the oauth signing code."""
2059
2060=== modified file 'ubuntuone/controlpanel/replication_client.py'
2061--- ubuntuone/controlpanel/replication_client.py 2011-01-07 20:07:39 +0000
2062+++ ubuntuone/controlpanel/replication_client.py 2011-04-08 19:43:13 +0000
2063@@ -57,7 +57,7 @@
2064 if replication_module is None:
2065 # delay import in case DC is not installed at module import time
2066 # Unable to import 'desktopcouch.application.replication_services'
2067- # pylint: disable=F0401
2068+ # pylint: disable=W0404
2069 from desktopcouch.application.replication_services \
2070 import ubuntuone as replication_module
2071 try:
2072
2073=== modified file 'ubuntuone/controlpanel/tests/__init__.py'
2074--- ubuntuone/controlpanel/tests/__init__.py 2011-03-10 02:59:44 +0000
2075+++ ubuntuone/controlpanel/tests/__init__.py 2011-04-08 19:43:13 +0000
2076@@ -23,7 +23,7 @@
2077
2078 TOKEN = {u'consumer_key': u'xQ7xDAz',
2079 u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy',
2080- u'token_name': u'test',
2081+ u'name': u'Ubuntu One @ localhost',
2082 u'token': u'ABCDEF01234-localtoken',
2083 u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
2084
2085@@ -110,19 +110,31 @@
2086 ]
2087 """
2088
2089+EMPTY_DESCRIPTION_JSON = """
2090+[
2091+ {
2092+ "token": "ABCDEF01234token",
2093+ "description": null,
2094+ "kind": "Computer"
2095+ }
2096+]
2097+"""
2098+
2099+LOCAL_DEVICE = {
2100+ 'is_local': 'True',
2101+ 'configurable': 'True',
2102+ 'device_id': 'ComputerABCDEF01234-localtoken',
2103+ 'limit_bandwidth': '',
2104+ 'show_all_notifications': 'True',
2105+ 'max_download_speed': '-1',
2106+ 'max_upload_speed': '-1',
2107+ 'name': 'Ubuntu One @ localhost',
2108+ 'type': 'Computer',
2109+}
2110+
2111 # note that local computer should be first, do not change!
2112 EXPECTED_DEVICES_INFO = [
2113- {
2114- 'is_local': 'True',
2115- 'configurable': 'True',
2116- 'device_id': 'ComputerABCDEF01234-localtoken',
2117- 'limit_bandwidth': '',
2118- 'show_all_notifications': 'True',
2119- 'max_download_speed': '-1',
2120- 'max_upload_speed': '-1',
2121- 'name': 'Ubuntu One @ localhost',
2122- 'type': 'Computer',
2123- },
2124+ LOCAL_DEVICE,
2125 {
2126 "device_id": "ComputerABCDEF01234token",
2127 "name": "Ubuntu One @ darkstar",
2128
2129=== modified file 'ubuntuone/controlpanel/tests/test_backend.py'
2130--- ubuntuone/controlpanel/tests/test_backend.py 2011-03-10 02:59:44 +0000
2131+++ ubuntuone/controlpanel/tests/test_backend.py 2011-04-08 19:43:13 +0000
2132@@ -30,6 +30,7 @@
2133 from ubuntuone.controlpanel import backend, replication_client
2134 from ubuntuone.controlpanel.backend import (bool_str,
2135 ACCOUNT_API, DEVICES_API, DEVICE_REMOVE_API, QUOTA_API,
2136+ DEVICE_TYPE_COMPUTER,
2137 FILE_SYNC_DISABLED,
2138 FILE_SYNC_DISCONNECTED,
2139 FILE_SYNC_ERROR,
2140@@ -41,9 +42,11 @@
2141 MSG_KEY, STATUS_KEY,
2142 )
2143 from ubuntuone.controlpanel.tests import (TestCase,
2144+ EMPTY_DESCRIPTION_JSON,
2145 EXPECTED_ACCOUNT_INFO,
2146 EXPECTED_ACCOUNT_INFO_WITH_CURRENT_PLAN,
2147 EXPECTED_DEVICES_INFO,
2148+ LOCAL_DEVICE,
2149 ROOT_PATH,
2150 SAMPLE_ACCOUNT_NO_CURRENT_PLAN,
2151 SAMPLE_ACCOUNT_WITH_CURRENT_PLAN,
2152@@ -56,7 +59,6 @@
2153 SHARES_PATH_LINK,
2154 TOKEN,
2155 )
2156-from ubuntuone.controlpanel.webclient import WebClientError
2157
2158
2159 class MockWebClient(object):
2160@@ -70,8 +72,10 @@
2161
2162 def call_api(self, method):
2163 """Get a given url from the webservice."""
2164- if self.failure:
2165- return defer.fail(WebClientError(self.failure))
2166+ if self.failure == 401:
2167+ return defer.fail(backend.UnauthorizedError(self.failure))
2168+ elif self.failure:
2169+ return defer.fail(backend.WebClientError(self.failure))
2170 else:
2171 result = simplejson.loads(self.results[method])
2172 return defer.succeed(result)
2173@@ -250,7 +254,7 @@
2174 self.patch(backend, "WebClient", MockWebClient)
2175 self.patch(backend, "dbus_client", MockDBusClient())
2176 self.patch(backend, "replication_client", MockReplicationClient())
2177- self.local_token = "Computer" + TOKEN["token"]
2178+ self.local_token = DEVICE_TYPE_COMPUTER + TOKEN["token"]
2179 self.be = backend.ControlBackend()
2180
2181 self.memento = MementoHandler()
2182@@ -278,6 +282,24 @@
2183 result = yield self.be.device_is_local(did)
2184 self.assertFalse(result)
2185
2186+ def test_shutdown_func(self):
2187+ """A shutdown_func can be passed as creation parameter."""
2188+ f = lambda: None
2189+ be = backend.ControlBackend(shutdown_func=f)
2190+ self.assertEqual(be.shutdown_func, f)
2191+
2192+ def test_shutdown_func_is_called_on_shutdown(self):
2193+ """The shutdown_func is called on shutdown."""
2194+ self.be.shutdown_func = self._set_called
2195+ self.be.shutdown()
2196+ self.assertEqual(self._called, ((), {}))
2197+
2198+ def test_shutdown_func_when_none(self):
2199+ """The shutdown_func can be None."""
2200+ self.be.shutdown_func = None
2201+ self.be.shutdown()
2202+ # nothing explodes
2203+
2204
2205 class BackendAccountTestCase(BackendBasicTestCase):
2206 """Account tests for the backend."""
2207@@ -305,7 +327,20 @@
2208 """The account_info method exercises its errback."""
2209 # pylint: disable=E1101
2210 self.be.wc.failure = 404
2211- yield self.assertFailure(self.be.account_info(), WebClientError)
2212+ yield self.assertFailure(self.be.account_info(),
2213+ backend.WebClientError)
2214+
2215+ @inlineCallbacks
2216+ def test_account_info_fails_with_unauthorized(self):
2217+ """The account_info clears the credentials on unauthorized."""
2218+ # pylint: disable=E1101
2219+ self.be.wc.failure = 401
2220+ d = defer.Deferred()
2221+ self.patch(backend.dbus_client, 'clear_credentials',
2222+ lambda: d.callback('called'))
2223+ yield self.assertFailure(self.be.account_info(),
2224+ backend.UnauthorizedError)
2225+ yield d
2226
2227
2228 class BackendDevicesTestCase(BackendBasicTestCase):
2229@@ -322,14 +357,86 @@
2230 @inlineCallbacks
2231 def test_devices_info_fails(self):
2232 """The devices_info method exercises its errback."""
2233+ def fail(*args, **kwargs):
2234+ """Raise any error other than WebClientError."""
2235+ raise ValueError(args)
2236+
2237+ self.patch(self.be.wc, 'call_api', fail)
2238+ yield self.assertFailure(self.be.devices_info(), ValueError)
2239+
2240+ @inlineCallbacks
2241+ def test_devices_info_with_webclient_error(self):
2242+ """The devices_info returns local info if webclient error."""
2243 # pylint: disable=E1101
2244 self.be.wc.failure = 404
2245- yield self.assertFailure(self.be.devices_info(), WebClientError)
2246+ result = yield self.be.devices_info()
2247+
2248+ self.assertEqual(result, [LOCAL_DEVICE])
2249+ self.assertTrue(self.memento.check_error('devices_info',
2250+ 'web client failure'))
2251+
2252+ @inlineCallbacks
2253+ def test_devices_info_fails_with_unauthorized(self):
2254+ """The devices_info clears the credentials on unauthorized."""
2255+ # pylint: disable=E1101
2256+ self.be.wc.failure = 401
2257+ d = defer.Deferred()
2258+ self.patch(backend.dbus_client, 'clear_credentials',
2259+ lambda: d.callback('called'))
2260+ yield self.assertFailure(self.be.devices_info(),
2261+ backend.UnauthorizedError)
2262+ yield d
2263+
2264+ @inlineCallbacks
2265+ def test_devices_info_if_files_disable(self):
2266+ """The devices_info returns device only info if files is disabled."""
2267+ yield self.be.disable_files()
2268+ status = yield self.be.file_sync_status()
2269+ assert status['status'] == backend.FILE_SYNC_DISABLED, status
2270+
2271+ # pylint: disable=E1101
2272+ self.be.wc.results[DEVICES_API] = SAMPLE_DEVICES_JSON
2273+ result = yield self.be.devices_info()
2274+
2275+ expected = EXPECTED_DEVICES_INFO[:]
2276+ for device in expected:
2277+ device.pop('limit_bandwidth', None)
2278+ device.pop('max_download_speed', None)
2279+ device.pop('max_upload_speed', None)
2280+ device.pop('show_all_notifications', None)
2281+ device['configurable'] = ''
2282+ self.assertEqual(result, expected)
2283+
2284+ @inlineCallbacks
2285+ def test_devices_info_when_token_name_is_empty(self):
2286+ """The devices_info can handle empty token names."""
2287+ # pylint: disable=E1101
2288+ self.be.wc.results[DEVICES_API] = EMPTY_DESCRIPTION_JSON
2289+ result = yield self.be.devices_info()
2290+ expected = {'configurable': '',
2291+ 'device_id': 'ComputerABCDEF01234token',
2292+ 'is_local': '', 'name': 'None',
2293+ 'type': DEVICE_TYPE_COMPUTER}
2294+ self.assertEqual(result, [expected])
2295+ self.assertTrue(self.memento.check_warning('name', 'None'))
2296+
2297+ @inlineCallbacks
2298+ def test_devices_info_does_not_log_device_id(self):
2299+ """The devices_info does not log the device_id."""
2300+ # pylint: disable=E1101
2301+ self.be.wc.results[DEVICES_API] = SAMPLE_DEVICES_JSON
2302+ yield self.be.devices_info()
2303+
2304+ dids = (d['device_id'] for d in EXPECTED_DEVICES_INFO)
2305+ device_id_logged = all(all(did not in r.getMessage()
2306+ for r in self.memento.records)
2307+ for did in dids)
2308+ self.assertTrue(device_id_logged)
2309
2310 @inlineCallbacks
2311 def test_remove_device(self):
2312 """The remove_device method calls the right api."""
2313- dtype, did = "Computer", "SAMPLE-TOKEN"
2314+ dtype, did = DEVICE_TYPE_COMPUTER, "SAMPLE-TOKEN"
2315 device_id = dtype + did
2316 apiurl = DEVICE_REMOVE_API % (dtype.lower(), did)
2317 # pylint: disable=E1101
2318@@ -354,7 +461,30 @@
2319 """The remove_device method fails as expected."""
2320 # pylint: disable=E1101
2321 self.be.wc.failure = 404
2322- yield self.assertFailure(self.be.devices_info(), WebClientError)
2323+ yield self.assertFailure(self.be.remove_device(self.local_token),
2324+ backend.WebClientError)
2325+
2326+ @inlineCallbacks
2327+ def test_remove_device_fails_with_unauthorized(self):
2328+ """The remove_device clears the credentials on unauthorized."""
2329+ # pylint: disable=E1101
2330+ self.be.wc.failure = 401
2331+ d = defer.Deferred()
2332+ self.patch(backend.dbus_client, 'clear_credentials',
2333+ lambda: d.callback('called'))
2334+ yield self.assertFailure(self.be.remove_device(self.local_token),
2335+ backend.UnauthorizedError)
2336+ yield d
2337+
2338+ @inlineCallbacks
2339+ def test_remove_device_does_not_log_device_id(self):
2340+ """The remove_device does not log the device_id."""
2341+ device_id = DEVICE_TYPE_COMPUTER + TOKEN['token']
2342+ yield self.be.remove_device(device_id)
2343+
2344+ device_id_logged = all(device_id not in r.getMessage()
2345+ for r in self.memento.records)
2346+ self.assertTrue(device_id_logged)
2347
2348 @inlineCallbacks
2349 def test_change_show_all_notifications(self):
2350@@ -411,6 +541,16 @@
2351 self.assertEqual(backend.dbus_client.limits["upload"], -1)
2352 self.assertEqual(backend.dbus_client.limits["download"], -1)
2353
2354+ @inlineCallbacks
2355+ def test_changing_settings_does_not_log_device_id(self):
2356+ """The change_device_settings does not log the device_id."""
2357+ device_id = 'yadda-yadda'
2358+ yield self.be.change_device_settings(device_id, {})
2359+
2360+ device_id_logged = all(device_id not in r.getMessage()
2361+ for r in self.memento.records)
2362+ self.assertTrue(device_id_logged)
2363+
2364
2365 class BackendVolumesTestCase(BackendBasicTestCase):
2366 """Volumes tests for the backend."""
2367@@ -581,6 +721,12 @@
2368 class BackendSyncStatusTestCase(BackendBasicTestCase):
2369 """Syncdaemon state for the backend."""
2370
2371+ was_disabled = False
2372+
2373+ def setUp(self):
2374+ super(BackendSyncStatusTestCase, self).setUp()
2375+ self.be.file_sync_disabled = self.was_disabled
2376+
2377 def _build_msg(self):
2378 """Build expected message regarding file sync status."""
2379 return '%s (%s)' % (MockDBusClient.status['description'],
2380@@ -599,6 +745,7 @@
2381 """The syncdaemon status is processed and emitted."""
2382 self.patch(MockDBusClient, 'file_sync', False)
2383 yield self.assert_correct_status(FILE_SYNC_DISABLED, msg='')
2384+ self.assertTrue(self.be.file_sync_disabled)
2385
2386 @inlineCallbacks
2387 def test_error(self):
2388@@ -610,6 +757,8 @@
2389 'description': 'auth failed',
2390 }
2391 yield self.assert_correct_status(FILE_SYNC_ERROR)
2392+ # self.be.file_sync_disabled does not change
2393+ self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
2394
2395 @inlineCallbacks
2396 def test_starting_when_init_not_user(self):
2397@@ -620,6 +769,7 @@
2398 'name': 'INIT', 'description': 'something new',
2399 }
2400 yield self.assert_correct_status(FILE_SYNC_STARTING)
2401+ self.assertFalse(self.be.file_sync_disabled)
2402
2403 @inlineCallbacks
2404 def test_starting_when_init_with_user(self):
2405@@ -630,6 +780,7 @@
2406 'name': 'INIT', 'description': 'something new',
2407 }
2408 yield self.assert_correct_status(FILE_SYNC_STARTING)
2409+ self.assertFalse(self.be.file_sync_disabled)
2410
2411 @inlineCallbacks
2412 def test_starting_when_local_rescan_not_user(self):
2413@@ -640,6 +791,7 @@
2414 'name': 'LOCAL_RESCAN', 'description': 'something new',
2415 }
2416 yield self.assert_correct_status(FILE_SYNC_STARTING)
2417+ self.assertFalse(self.be.file_sync_disabled)
2418
2419 @inlineCallbacks
2420 def test_starting_when_local_rescan_with_user(self):
2421@@ -650,6 +802,7 @@
2422 'name': 'LOCAL_RESCAN', 'description': 'something new',
2423 }
2424 yield self.assert_correct_status(FILE_SYNC_STARTING)
2425+ self.assertFalse(self.be.file_sync_disabled)
2426
2427 @inlineCallbacks
2428 def test_starting_when_ready_with_user(self):
2429@@ -660,6 +813,7 @@
2430 'name': 'READY', 'description': 'something nicer',
2431 }
2432 yield self.assert_correct_status(FILE_SYNC_STARTING)
2433+ self.assertFalse(self.be.file_sync_disabled)
2434
2435 @inlineCallbacks
2436 def test_disconnected(self):
2437@@ -672,6 +826,9 @@
2438 }
2439 yield self.assert_correct_status(FILE_SYNC_DISCONNECTED)
2440
2441+ # self.be.file_sync_disabled does not change
2442+ self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
2443+
2444 @inlineCallbacks
2445 def test_disconnected_when_waiting(self):
2446 """The syncdaemon status is processed and emitted."""
2447@@ -682,6 +839,9 @@
2448 }
2449 yield self.assert_correct_status(FILE_SYNC_DISCONNECTED)
2450
2451+ # self.be.file_sync_disabled does not change
2452+ self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
2453+
2454 @inlineCallbacks
2455 def test_syncing_if_online(self):
2456 """The syncdaemon status is processed and emitted."""
2457@@ -693,6 +853,9 @@
2458 }
2459 yield self.assert_correct_status(FILE_SYNC_SYNCING)
2460
2461+ # self.be.file_sync_disabled does not change
2462+ self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
2463+
2464 @inlineCallbacks
2465 def test_syncing_even_if_not_online(self):
2466 """The syncdaemon status is processed and emitted."""
2467@@ -704,6 +867,9 @@
2468 }
2469 yield self.assert_correct_status(FILE_SYNC_SYNCING)
2470
2471+ # self.be.file_sync_disabled does not change
2472+ self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
2473+
2474 @inlineCallbacks
2475 def test_idle(self):
2476 """The syncdaemon status is processed and emitted."""
2477@@ -715,6 +881,9 @@
2478 }
2479 yield self.assert_correct_status(FILE_SYNC_IDLE)
2480
2481+ # self.be.file_sync_disabled does not change
2482+ self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
2483+
2484 @inlineCallbacks
2485 def test_stopped(self):
2486 """The syncdaemon status is processed and emitted."""
2487@@ -726,6 +895,9 @@
2488 }
2489 yield self.assert_correct_status(FILE_SYNC_STOPPED)
2490
2491+ # self.be.file_sync_disabled does not change
2492+ self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
2493+
2494 @inlineCallbacks
2495 def test_unknown(self):
2496 """The syncdaemon status is processed and emitted."""
2497@@ -740,6 +912,9 @@
2498 repr(MockDBusClient.status))
2499 self.assertTrue(has_warning)
2500
2501+ # self.be.file_sync_disabled does not change
2502+ self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
2503+
2504 def test_status_changed(self):
2505 """The file_sync_status is the status changed handler."""
2506 self.be.status_changed_handler = self._set_called
2507@@ -754,6 +929,21 @@
2508 self.assertEqual(self._called, ((expected_status,), {}))
2509
2510
2511+class BackendSyncStatusIfDisabledTestCase(BackendSyncStatusTestCase):
2512+ """Syncdaemon state for the backend when file sync is disabled."""
2513+
2514+ was_disabled = True
2515+
2516+ @inlineCallbacks
2517+ def assert_correct_status(self, status, msg=None):
2518+ """Check that the resulting status is correct."""
2519+ sup = super(BackendSyncStatusIfDisabledTestCase, self)
2520+ if status != FILE_SYNC_STARTING:
2521+ yield sup.assert_correct_status(FILE_SYNC_DISABLED, msg='')
2522+ else:
2523+ yield sup.assert_correct_status(status, msg=msg)
2524+
2525+
2526 class BackendFileSyncOpsTestCase(BackendBasicTestCase):
2527 """Syncdaemon operations for the backend."""
2528
2529@@ -768,6 +958,7 @@
2530
2531 yield self.be.enable_files()
2532 self.assertTrue(MockDBusClient.file_sync)
2533+ self.assertFalse(self.be.file_sync_disabled)
2534
2535 @inlineCallbacks
2536 def test_disable_files(self):
2537@@ -776,6 +967,7 @@
2538
2539 yield self.be.disable_files()
2540 self.assertFalse(MockDBusClient.file_sync)
2541+ self.assertTrue(self.be.file_sync_disabled)
2542
2543 @inlineCallbacks
2544 def test_connect_files(self):
2545@@ -783,6 +975,7 @@
2546 yield self.be.connect_files()
2547
2548 self.assertEqual(MockDBusClient.actions, ['connect'])
2549+ self.assertFalse(self.be.file_sync_disabled)
2550
2551 @inlineCallbacks
2552 def test_disconnect_files(self):
2553@@ -790,6 +983,7 @@
2554 yield self.be.disconnect_files()
2555
2556 self.assertEqual(MockDBusClient.actions, ['disconnect'])
2557+ self.assertFalse(self.be.file_sync_disabled)
2558
2559 @inlineCallbacks
2560 def test_restart_files(self):
2561@@ -797,6 +991,7 @@
2562 yield self.be.restart_files()
2563
2564 self.assertEqual(MockDBusClient.actions, ['stop', 'start'])
2565+ self.assertFalse(self.be.file_sync_disabled)
2566
2567 @inlineCallbacks
2568 def test_start_files(self):
2569@@ -804,6 +999,7 @@
2570 yield self.be.start_files()
2571
2572 self.assertEqual(MockDBusClient.actions, ['start'])
2573+ self.assertFalse(self.be.file_sync_disabled)
2574
2575 @inlineCallbacks
2576 def test_stop_files(self):
2577@@ -811,6 +1007,7 @@
2578 yield self.be.stop_files()
2579
2580 self.assertEqual(MockDBusClient.actions, ['stop'])
2581+ self.assertFalse(self.be.file_sync_disabled)
2582
2583
2584 class BackendReplicationsTestCase(BackendBasicTestCase):
2585
2586=== modified file 'ubuntuone/controlpanel/utils.py'
2587--- ubuntuone/controlpanel/utils.py 2010-12-22 13:33:25 +0000
2588+++ ubuntuone/controlpanel/utils.py 2011-04-08 19:43:13 +0000
2589@@ -52,7 +52,7 @@
2590
2591 # otherwise, try to load PROJECT_DIR from installation path
2592 try:
2593- # pylint: disable=F0401, E0611
2594+ # pylint: disable=F0401, E0611, W0404
2595 from ubuntuone.controlpanel.constants import PROJECT_DIR
2596 return PROJECT_DIR
2597 except ImportError:
2598
2599=== modified file 'ubuntuone/controlpanel/webclient.py'
2600--- ubuntuone/controlpanel/webclient.py 2010-12-22 13:33:25 +0000
2601+++ ubuntuone/controlpanel/webclient.py 2011-04-08 19:43:13 +0000
2602@@ -32,10 +32,18 @@
2603 logger = setup_logging('webclient')
2604
2605
2606+# full list of status codes
2607+# http://library.gnome.org/devel/libsoup/stable/libsoup-2.4-soup-status.html
2608+
2609+
2610 class WebClientError(Exception):
2611 """An http error happened while calling the webservice."""
2612
2613
2614+class UnauthorizedError(WebClientError):
2615+ """The request ended with bad_request, unauthorized or forbidden."""
2616+
2617+
2618 class WebClient(object):
2619 """A client for the u1 webservice."""
2620
2621@@ -48,21 +56,24 @@
2622
2623 def _handler(self, session, msg, d):
2624 """Handle the result of an http message."""
2625- logger.debug("WebClient: got http response %d for uri %r",
2626+ logger.debug("got http response %d for uri %r",
2627 msg.status_code, msg.get_uri().to_string(False))
2628 data = msg.response_body.data
2629 if msg.status_code == 200:
2630 result = simplejson.loads(data)
2631 d.callback(result)
2632 else:
2633- e = WebClientError(msg.status_code, data)
2634+ if msg.status_code in (401,):
2635+ e = UnauthorizedError(msg.status_code, data)
2636+ else:
2637+ e = WebClientError(msg.status_code, data)
2638 d.errback(e)
2639
2640 def _call_api_with_creds(self, credentials, api_name):
2641 """Get a given url from the webservice with credentials."""
2642 url = (self.base_url + api_name).encode('utf-8')
2643 method = "GET"
2644- logger.debug("WebClient: getting url: %s, %s", method, url)
2645+ logger.debug("getting url: %s, %s", method, url)
2646 msg = Soup.Message.new(method, url)
2647 add_oauth_headers(msg.request_headers.append, method, url, credentials)
2648 d = defer.Deferred()
2649@@ -71,7 +82,8 @@
2650
2651 def call_api(self, api_name):
2652 """Get a given url from the webservice."""
2653- logger.debug("WebClient: calling api: %s", api_name)
2654+ # this may log device ID's, but only for removals, which is OK
2655+ logger.debug("calling api: %s", api_name)
2656 d = self.get_credentials()
2657 d.addCallback(self._call_api_with_creds, api_name)
2658 return d

Subscribers

People subscribed via source and target branches