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
=== modified file 'PKG-INFO'
--- PKG-INFO 2011-03-23 20:33:42 +0000
+++ PKG-INFO 2011-04-08 19:43:13 +0000
@@ -1,6 +1,6 @@
1Metadata-Version: 1.11Metadata-Version: 1.1
2Name: ubuntuone-control-panel2Name: ubuntuone-control-panel
3Version: 0.9.43Version: 0.9.5
4Summary: Ubuntu One Control Panel4Summary: Ubuntu One Control Panel
5Home-page: https://launchpad.net/ubuntuone-control-panel5Home-page: https://launchpad.net/ubuntuone-control-panel
6Author: Natalia Bidart6Author: Natalia Bidart
77
=== modified file 'bin/ubuntuone-control-panel-gtk'
--- bin/ubuntuone-control-panel-gtk 2011-03-23 16:06:41 +0000
+++ bin/ubuntuone-control-panel-gtk 2011-04-08 19:43:13 +0000
@@ -2,6 +2,7 @@
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
33
4# Authors: Natalia B Bidart <natalia.bidart@canonical.com>4# Authors: Natalia B Bidart <natalia.bidart@canonical.com>
5# Eric Casteleijn <eric.casteleijn@canonical.com>
5#6#
6# Copyright 2010 Canonical Ltd.7# Copyright 2010 Canonical Ltd.
7#8#
@@ -20,20 +21,16 @@
2021
21# Invalid name "ubuntuone-control-panel-gtk", pylint: disable=C010322# Invalid name "ubuntuone-control-panel-gtk", pylint: disable=C0103
2223
24import gettext
23import sys25import sys
2426
25import dbus.mainloop.glib
26import gettext
27
28from optparse import OptionParser27from optparse import OptionParser
2928
30from ubuntuone.controlpanel.gtk import DBUS_BUS_NAME, TRANSLATION_DOMAIN29from ubuntuone.controlpanel.gtk import TRANSLATION_DOMAIN
3130
32dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
33gettext.textdomain(TRANSLATION_DOMAIN)31gettext.textdomain(TRANSLATION_DOMAIN)
34
35# import the GUI after the translation domain has been set32# import the GUI after the translation domain has been set
36from ubuntuone.controlpanel.gtk.gui import ControlPanelWindow33from ubuntuone.controlpanel.gtk.gui import main
3734
3835
39def parser_options():36def parser_options():
@@ -41,26 +38,17 @@
41 usage = "Usage: %prog [option]"38 usage = "Usage: %prog [option]"
42 result = OptionParser(usage=usage)39 result = OptionParser(usage=usage)
43 result.add_option("", "--switch-to", dest="switch_to", type="string",40 result.add_option("", "--switch-to", dest="switch_to", type="string",
44 metavar="PANEL_NAME",41 metavar="PANEL_NAME", default="",
45 help="Start the Ubuntu One Control Panel (GTK) in the "42 help="Start the Ubuntu One Control Panel (GTK) in the "
46 "PANEL_NAME tab. Possible values are: "43 "PANEL_NAME tab. Possible values are: "
47 "dashboard, volumes, devices, applications")44 "dashboard, volumes, devices, applications")
48 result.add_option("-a", "--alert", dest="alert", action="store_true",45 result.add_option("-a", "--alert", dest="alert", action="store_true",
49 help="Start the Ubuntu One Control Panel (GTK) alerting "46 default=False, help="Start the Ubuntu One Control Panel "
50 "the user to its presence.")47 "(GTK) alerting the user to its presence.")
51 return result48 return result
5249
5350
54if __name__ == "__main__":51if __name__ == "__main__":
55 bus = dbus.SessionBus()
56 name = bus.request_name(DBUS_BUS_NAME,
57 dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
58 if name == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
59 sys.exit(0)
60
61 bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
62 parser = parser_options()52 parser = parser_options()
63 (options, args) = parser.parse_args(sys.argv)53 (options, args) = parser.parse_args(sys.argv)
64 gui = ControlPanelWindow(54 main(switch_to=options.switch_to, alert=options.alert)
65 switch_to=options.switch_to, alert=options.alert)
66 gui.main()
6755
=== modified file 'debian/changelog'
--- debian/changelog 2011-03-23 20:44:08 +0000
+++ debian/changelog 2011-04-08 19:43:13 +0000
@@ -1,3 +1,45 @@
1ubuntuone-control-panel (0.9.5-0ubuntu1) UNRELEASED; urgency=low
2
3 * New upstream release:
4
5 [ eric.casteleijn@canonical.com ]
6 - Now that we set the launcher urgency from ubuntuone-client, the control
7 panel needs to remove it when its window receives focus (LP: #747677).
8 - changed default value for switch_to to empty string, and now don't call
9 switch_to method when the value is empty string (or anything else falsy)
10 (LP: #752943).
11 - Added proper defaults to the command line arguments (LP: #746489).
12 - Fixed issue where closing the panel resulted in a runtime error
13 (LP: #745987).
14 - This adds a method to the dbus service that allows switching between
15 panels, and/or drawing attention to the control panel (LP: #742008).
16 - Removed the shortcut group that causes two Ubuntu One entries to appear
17 in the messaging menu when syncdaemon is not running (LP: #721525).
18 [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
19 - If servers reply with a 401, clear credentials and ask user to
20 authenticate (LP: #726612).
21 - Moving style_check down so the exit code from u1trial is not hidden by
22 && operator.
23 - Unify disable/enable file sync functionality among Services tab and
24 global file sync status (LP: #729301).
25 - Cloud Folders tab is now disabled when the file sync service is
26 (LP: #747482).
27 - Improving legend for plugin installation to ease translations
28 (LP: #746374).
29 - Added volumes.ui to the translation list (LP: #746370).
30 - Small improvement to show something else besides the generic "Value can
31 not be retrieved." error (LP: #722485).
32 - Made the backend robust against possible None values (or any non
33 basestring instance) sent from the API server (LP: #745790).
34 - Decoupled device list retrieved from the web from the local settings
35 retrieved from syncdaemon (LP: #720704).
36 - Stop the control panel backend once the UI is done
37 (LP: #704434).
38 - After initial computer adding, syncdameon is asked to connect
39 (LP: #715873).
40
41 -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Fri, 08 Apr 2011 15:53:02 -0300
42
1ubuntuone-control-panel (0.9.4-0ubuntu1) natty; urgency=low43ubuntuone-control-panel (0.9.4-0ubuntu1) natty; urgency=low
244
3 * New upstream release:45 * New upstream release:
446
=== modified file 'po/POTFILES.in'
--- po/POTFILES.in 2011-01-07 20:07:39 +0000
+++ po/POTFILES.in 2011-04-08 19:43:13 +0000
@@ -7,3 +7,4 @@
7[type: gettext/glade] data/management.ui7[type: gettext/glade] data/management.ui
8[type: gettext/glade] data/overview.ui8[type: gettext/glade] data/overview.ui
9[type: gettext/glade] data/services.ui9[type: gettext/glade] data/services.ui
10[type: gettext/glade] data/volumes.ui
1011
=== modified file 'run-tests'
--- run-tests 2010-12-06 12:27:11 +0000
+++ run-tests 2011-04-08 19:43:13 +0000
@@ -19,19 +19,8 @@
19set -e19set -e
2020
21if [ $# -ne 0 ]; then21if [ $# -ne 0 ]; then
22 # an extra argument was given22 # run specific module given by the caller
23 if [ $1 == "--integration" ]; then23 MODULE="$@"
24 # run only integration tests
25 MODULE="ubuntuone/controlpanel/integrationtests"
26 else
27 if [ $1 == "--unittests" ]; then
28 # run only non-integration tests (unittests)
29 MODULE="ubuntuone/controlpanel/tests"
30 else
31 # run specific module given by the caller
32 MODULE="$@"
33 fi
34 fi
35else24else
36 # run all tests, useful for tarmac and reviews25 # run all tests, useful for tarmac and reviews
37 MODULE="ubuntuone/controlpanel"26 MODULE="ubuntuone/controlpanel"
@@ -40,12 +29,13 @@
40style_check() {29style_check() {
41 pylint ubuntuone/30 pylint ubuntuone/
42 if [ -x `which pep8` ]; then31 if [ -x `which pep8` ]; then
43 pep8 --repeat ubuntuone/32 pep8 --repeat bin/ $MODULE
44 else33 else
45 echo "Please install the 'pep8' package."34 echo "Please install the 'pep8' package."
46 fi35 fi
47}36}
4837
49echo "Running test suite for ""$MODULE"38echo "Running test suite for ""$MODULE"
50`which xvfb-run` u1trial "$MODULE" && style_check39`which xvfb-run` u1trial "$MODULE"
40style_check
51rm -rf _trial_temp41rm -rf _trial_temp
5242
=== modified file 'setup.py'
--- setup.py 2011-03-23 20:33:42 +0000
+++ setup.py 2011-04-08 19:43:13 +0000
@@ -79,7 +79,7 @@
7979
80DistUtilsExtra.auto.setup(80DistUtilsExtra.auto.setup(
81 name='ubuntuone-control-panel',81 name='ubuntuone-control-panel',
82 version='0.9.4',82 version='0.9.5',
83 license='GPL v3',83 license='GPL v3',
84 author='Natalia Bidart',84 author='Natalia Bidart',
85 author_email='natalia.bidart@canonical.com',85 author_email='natalia.bidart@canonical.com',
8686
=== modified file 'ubuntuone-control-panel-gtk.desktop.in'
--- ubuntuone-control-panel-gtk.desktop.in 2011-02-23 13:57:42 +0000
+++ ubuntuone-control-panel-gtk.desktop.in 2011-04-08 19:43:13 +0000
@@ -7,10 +7,4 @@
7Type=Application7Type=Application
8Categories=GNOME;GTK;Settings;8Categories=GNOME;GTK;Settings;
9StartupNotify=true9StartupNotify=true
10X-Ayatana-Desktop-Shortcuts=U1
11X-Ayatana-Appmenu-Show-Stubs=False10X-Ayatana-Appmenu-Show-Stubs=False
12
13[U1 Shortcut Group]
14Name=Ubuntu One
15Exec=ubuntuone-control-panel-gtk
16OnlyShowIn=Messaging Menu
1711
=== modified file 'ubuntuone/controlpanel/backend.py'
--- ubuntuone/controlpanel/backend.py 2011-03-10 02:59:44 +0000
+++ ubuntuone/controlpanel/backend.py 2011-04-08 19:43:13 +0000
@@ -20,14 +20,17 @@
20"""A backend for the Ubuntu One Control Panel."""20"""A backend for the Ubuntu One Control Panel."""
2121
22from collections import defaultdict22from collections import defaultdict
23from functools import wraps
2324
24from twisted.internet.defer import inlineCallbacks, returnValue25from twisted.internet.defer import inlineCallbacks, returnValue
2526
26from ubuntuone.controlpanel import dbus_client27from ubuntuone.controlpanel import dbus_client
27from ubuntuone.controlpanel import replication_client28from ubuntuone.controlpanel import replication_client
28from ubuntuone.controlpanel.logger import setup_logging, log_call29from ubuntuone.controlpanel.logger import setup_logging, log_call
29from ubuntuone.controlpanel.webclient import WebClient30# pylint: disable=W0611
3031from ubuntuone.controlpanel.webclient import (UnauthorizedError,
32 WebClient, WebClientError)
33# pylint: enable=W0611
3134
32logger = setup_logging('backend')35logger = setup_logging('backend')
3336
@@ -61,6 +64,35 @@
61 return 'True' if value else ''64 return 'True' if value else ''
6265
6366
67def filter_field(info, field):
68 """Return a copy of 'info' where each item has 'field' hidden."""
69 result = []
70 for item in info:
71 item = item.copy()
72 item[field] = '<hidden>'
73 result.append(item)
74 return result
75
76
77def process_unauthorized(f):
78 """Decorator to catch UnauthorizedError from the webclient and act upon."""
79
80 @inlineCallbacks
81 @wraps(f)
82 def inner(*args, **kwargs):
83 """Handle UnauthorizedError and clear credentials."""
84 try:
85 result = yield f(*args, **kwargs)
86 except UnauthorizedError, e:
87 logger.exception('process_unauthorized (clearing credentials):')
88 yield dbus_client.clear_credentials()
89 raise e
90
91 returnValue(result)
92
93 return inner
94
95
64class ControlBackend(object):96class ControlBackend(object):
65 """The control panel backend."""97 """The control panel backend."""
6698
@@ -68,14 +100,17 @@
68 FOLDER_TYPE = u'UDF'100 FOLDER_TYPE = u'UDF'
69 SHARE_TYPE = u'SHARE'101 SHARE_TYPE = u'SHARE'
70 NAME_NOT_SET = u'ENAMENOTSET'102 NAME_NOT_SET = u'ENAMENOTSET'
103 STATUS_DISABLED = {MSG_KEY: '', STATUS_KEY: FILE_SYNC_DISABLED}
71104
72 def __init__(self):105 def __init__(self, shutdown_func=None):
73 """Initialize the webclient."""106 """Initialize the webclient."""
107 self.shutdown_func = shutdown_func
74 self.wc = WebClient(dbus_client.get_credentials)108 self.wc = WebClient(dbus_client.get_credentials)
75 self._status_changed_handler = None109 self._status_changed_handler = None
76 self.status_changed_handler = lambda *a: None110 self.status_changed_handler = lambda *a: None
77111
78 self._volumes = {} # cache last known volume info112 self._volumes = {} # cache last known volume info
113 self.file_sync_disabled = False
79114
80 def _process_file_sync_status(self, status):115 def _process_file_sync_status(self, status):
81 """Process raw file sync status into custom format.116 """Process raw file sync status into custom format.
@@ -89,7 +124,8 @@
89124
90 """125 """
91 if not status:126 if not status:
92 return {MSG_KEY: '', STATUS_KEY: FILE_SYNC_DISABLED}127 self.file_sync_disabled = True
128 return self.STATUS_DISABLED
93129
94 msg = '%s (%s)' % (status['description'], status['name'])130 msg = '%s (%s)' % (status['description'], status['name'])
95 result = {MSG_KEY: msg}131 result = {MSG_KEY: msg}
@@ -113,6 +149,7 @@
113 elif is_disconnected:149 elif is_disconnected:
114 result[STATUS_KEY] = FILE_SYNC_DISCONNECTED150 result[STATUS_KEY] = FILE_SYNC_DISCONNECTED
115 elif is_starting:151 elif is_starting:
152 self.file_sync_disabled = False
116 result[STATUS_KEY] = FILE_SYNC_STARTING153 result[STATUS_KEY] = FILE_SYNC_STARTING
117 elif is_stopped:154 elif is_stopped:
118 result[STATUS_KEY] = FILE_SYNC_STOPPED155 result[STATUS_KEY] = FILE_SYNC_STOPPED
@@ -120,7 +157,10 @@
120 logger.warning('file_sync_status: unknown (got %r)', status)157 logger.warning('file_sync_status: unknown (got %r)', status)
121 result[STATUS_KEY] = FILE_SYNC_UNKNOWN158 result[STATUS_KEY] = FILE_SYNC_UNKNOWN
122159
123 return result160 if self.file_sync_disabled:
161 return self.STATUS_DISABLED
162 else:
163 return result
124164
125 def _set_status_changed_handler(self, handler):165 def _set_status_changed_handler(self, handler):
126 """Set 'handler' to be called when file sync status changes."""166 """Set 'handler' to be called when file sync status changes."""
@@ -142,6 +182,56 @@
142 _set_status_changed_handler)182 _set_status_changed_handler)
143183
144 @inlineCallbacks184 @inlineCallbacks
185 def _process_device_web_info(self, devices, enabled, limit_bw, limits,
186 show_notifs):
187 """Return a lis of processed devices."""
188 result = []
189 for d in devices:
190 di = {}
191 di["type"] = d["kind"]
192 di["name"] = d["description"]
193 di["configurable"] = ''
194 if di["type"] == DEVICE_TYPE_COMPUTER:
195 di["device_id"] = di["type"] + d["token"]
196 if di["type"] == DEVICE_TYPE_PHONE:
197 di["device_id"] = di["type"] + str(d["id"])
198
199 is_local = yield self.device_is_local(di["device_id"])
200 di["is_local"] = bool_str(is_local)
201 # currently, only local devices are configurable.
202 # eventually, more devices will be configurable.
203 di["configurable"] = bool_str(is_local and enabled)
204
205 if bool(di["configurable"]):
206 di["limit_bandwidth"] = bool_str(limit_bw)
207 di["show_all_notifications"] = bool_str(show_notifs)
208 upload = limits["upload"]
209 download = limits["download"]
210 di[UPLOAD_KEY] = str(upload)
211 di[DOWNLOAD_KEY] = str(download)
212
213 # date_added is not in the webservice yet (LP: #673668)
214 # di["date_added"] = ""
215
216 # missing values (LP: #673668)
217 # di["available_services"] = ""
218 # di["enabled_services"] = ""
219
220 # make a sanity check
221 for key, val in di.iteritems():
222 if not isinstance(val, basestring):
223 logger.warning('_process_device_web_info: (key %r), '
224 'val %r is not a basestring.', key, val)
225 di[key] = repr(val)
226
227 if is_local: # prepend the local device!
228 result.insert(0, di)
229 else:
230 result.append(di)
231
232 returnValue(result)
233
234 @inlineCallbacks
145 def get_token(self):235 def get_token(self):
146 """Return the token from the credentials."""236 """Return the token from the credentials."""
147 credentials = yield dbus_client.get_credentials()237 credentials = yield dbus_client.get_credentials()
@@ -153,10 +243,10 @@
153 dtype, did = self.type_n_id(device_id)243 dtype, did = self.type_n_id(device_id)
154 local_token = yield self.get_token()244 local_token = yield self.get_token()
155 is_local = (dtype == DEVICE_TYPE_COMPUTER and did == local_token)245 is_local = (dtype == DEVICE_TYPE_COMPUTER and did == local_token)
156 logger.info('device_is_local: result %r, ', is_local)
157 returnValue(is_local)246 returnValue(is_local)
158247
159 @log_call(logger.debug)248 @log_call(logger.debug)
249 @process_unauthorized
160 @inlineCallbacks250 @inlineCallbacks
161 def account_info(self):251 def account_info(self):
162 """Get the user account info."""252 """Get the user account info."""
@@ -184,50 +274,55 @@
184 returnValue(result)274 returnValue(result)
185275
186 @log_call(logger.debug)276 @log_call(logger.debug)
277 @process_unauthorized
187 @inlineCallbacks278 @inlineCallbacks
188 def devices_info(self):279 def devices_info(self):
189 """Get the user devices info."""280 """Get the user devices info."""
190 result = []281 result = limit_bw = limits = show_notifs = None
191 limit_bw = yield dbus_client.bandwidth_throttling_enabled()282 enabled = yield dbus_client.files_sync_enabled()
192 show_all_notif = yield dbus_client.show_all_notifications_enabled()283 if enabled:
193 limits = yield dbus_client.get_throttling_limits()284 limit_bw = yield dbus_client.bandwidth_throttling_enabled()
194285 show_notifs = yield dbus_client.show_all_notifications_enabled()
195 devices = yield self.wc.call_api(DEVICES_API)286 limits = yield dbus_client.get_throttling_limits()
196 for d in devices:287
197 di = {}288 logger.debug('devices_info: file sync enabled? %s limit_bw %s, limits '
198 di["type"] = d["kind"]289 '%s, show_notifs %s',
199 di["name"] = d["description"]290 enabled, limit_bw, limits, show_notifs)
200 di["configurable"] = ''291
201 if di["type"] == DEVICE_TYPE_COMPUTER:292 try:
202 di["device_id"] = di["type"] + d["token"]293 devices = yield self.wc.call_api(DEVICES_API)
203 if di["type"] == DEVICE_TYPE_PHONE:294 except UnauthorizedError:
204 di["device_id"] = di["type"] + str(d["id"])295 raise
205296 except WebClientError:
206 is_local = yield self.device_is_local(di["device_id"])297 logger.exception('devices_info: web client failure:')
207 di["is_local"] = bool_str(is_local)298 else:
208 # currently, only local devices are configurable.299 result = yield self._process_device_web_info(devices, enabled,
209 # eventually, more the devices will be configurable.300 limit_bw, limits,
210 di["configurable"] = bool_str(is_local)301 show_notifs)
211302 if result is None:
212 if bool(di["configurable"]):303 logger.info('devices_info: result is None after calling '
213 di["limit_bandwidth"] = bool_str(limit_bw)304 'devices/ API, building the local device.')
214 di["show_all_notifications"] = bool_str(show_all_notif)305 credentials = yield dbus_client.get_credentials()
306 local_device = {}
307 local_device["type"] = DEVICE_TYPE_COMPUTER
308 local_device["name"] = credentials['name']
309 device_id = local_device["type"] + credentials["token"]
310 local_device["device_id"] = device_id
311 local_device["is_local"] = bool_str(True)
312 local_device["configurable"] = bool_str(enabled)
313 if bool(local_device["configurable"]):
314 local_device["limit_bandwidth"] = bool_str(limit_bw)
315 show_notifs = bool_str(show_notifs)
316 local_device["show_all_notifications"] = show_notifs
215 upload = limits["upload"]317 upload = limits["upload"]
216 download = limits["download"]318 download = limits["download"]
217 di[UPLOAD_KEY] = str(upload)319 local_device[UPLOAD_KEY] = str(upload)
218 di[DOWNLOAD_KEY] = str(download)320 local_device[DOWNLOAD_KEY] = str(download)
219321 result = [local_device]
220 # date_added is not in the webservice yet (LP: #673668)322 else:
221 # di["date_added"] = ""323 logger.info('devices_info: result is not None after calling '
222324 'devices/ API: %r',
223 # missing values (LP: #673668)325 filter_field(result, field='device_id'))
224 # di["available_services"] = ""
225 # di["enabled_services"] = ""
226
227 if is_local: # prepend the local device!
228 result.insert(0, di)
229 else:
230 result.append(di)
231326
232 returnValue(result)327 returnValue(result)
233328
@@ -239,7 +334,7 @@
239 return DEVICE_TYPE_PHONE, device_id[5:]334 return DEVICE_TYPE_PHONE, device_id[5:]
240 return "No device", device_id335 return "No device", device_id
241336
242 @log_call(logger.info)337 @log_call(logger.info, with_args=False)
243 @inlineCallbacks338 @inlineCallbacks
244 def change_device_settings(self, device_id, settings):339 def change_device_settings(self, device_id, settings):
245 """Change the settings for the given device."""340 """Change the settings for the given device."""
@@ -275,7 +370,8 @@
275 # still pending: more work on the settings dict (LP: #673674)370 # still pending: more work on the settings dict (LP: #673674)
276 returnValue(device_id)371 returnValue(device_id)
277372
278 @log_call(logger.warning)373 @log_call(logger.warning, with_args=False)
374 @process_unauthorized
279 @inlineCallbacks375 @inlineCallbacks
280 def remove_device(self, device_id):376 def remove_device(self, device_id):
281 """Remove a device's tokens from the sso server."""377 """Remove a device's tokens from the sso server."""
@@ -286,8 +382,8 @@
286 yield self.wc.call_api(api)382 yield self.wc.call_api(api)
287383
288 if is_local:384 if is_local:
289 logger.warning('remove_device: device is local, id %r, '385 logger.warning('remove_device: device is local! removing and '
290 'clearing credentials.', device_id)386 'clearing credentials.')
291 yield dbus_client.clear_credentials()387 yield dbus_client.clear_credentials()
292388
293 returnValue(device_id)389 returnValue(device_id)
@@ -308,12 +404,14 @@
308 def enable_files(self):404 def enable_files(self):
309 """Enable the files service."""405 """Enable the files service."""
310 yield dbus_client.set_files_sync_enabled(True)406 yield dbus_client.set_files_sync_enabled(True)
407 self.file_sync_disabled = False
311408
312 @log_call(logger.debug)409 @log_call(logger.debug)
313 @inlineCallbacks410 @inlineCallbacks
314 def disable_files(self):411 def disable_files(self):
315 """Enable the files service."""412 """Enable the files service."""
316 yield dbus_client.set_files_sync_enabled(False)413 yield dbus_client.set_files_sync_enabled(False)
414 self.file_sync_disabled = True
317415
318 @log_call(logger.debug)416 @log_call(logger.debug)
319 @inlineCallbacks417 @inlineCallbacks
@@ -478,3 +576,10 @@
478 """Install the extension to sync bookmarks."""576 """Install the extension to sync bookmarks."""
479 # still pending (LP: #673673)577 # still pending (LP: #673673)
480 returnValue(None)578 returnValue(None)
579
580 @log_call(logger.info)
581 def shutdown(self):
582 """Stop this service."""
583 # do any other needed cleanup
584 if self.shutdown_func is not None:
585 self.shutdown_func()
481586
=== modified file 'ubuntuone/controlpanel/dbus_client.py'
--- ubuntuone/controlpanel/dbus_client.py 2011-02-23 13:57:42 +0000
+++ ubuntuone/controlpanel/dbus_client.py 2011-04-08 19:43:13 +0000
@@ -64,7 +64,6 @@
6464
65 def found_credentials(app_name, creds):65 def found_credentials(app_name, creds):
66 """Credentials have been found."""66 """Credentials have been found."""
67 logger.debug('credentials were found for app_name %r.', app_name)
68 if app_name == APP_NAME:67 if app_name == APP_NAME:
69 logger.info('credentials were found! (%r).', APP_NAME)68 logger.info('credentials were found! (%r).', APP_NAME)
70 d.callback(creds)69 d.callback(creds)
7170
=== modified file 'ubuntuone/controlpanel/dbus_service.py'
--- ubuntuone/controlpanel/dbus_service.py 2011-01-25 19:08:59 +0000
+++ ubuntuone/controlpanel/dbus_service.py 2011-04-08 19:43:13 +0000
@@ -32,7 +32,8 @@
32from ubuntuone.controlpanel import (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH,32from ubuntuone.controlpanel import (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH,
33 DBUS_PREFERENCES_IFACE)33 DBUS_PREFERENCES_IFACE)
34from ubuntuone.controlpanel.backend import (34from ubuntuone.controlpanel.backend import (
35 ControlBackend, FILE_SYNC_DISABLED, FILE_SYNC_DISCONNECTED,35 ControlBackend, filter_field, UnauthorizedError,
36 FILE_SYNC_DISABLED, FILE_SYNC_DISCONNECTED,
36 FILE_SYNC_ERROR, FILE_SYNC_IDLE, FILE_SYNC_STARTING, FILE_SYNC_STOPPED,37 FILE_SYNC_ERROR, FILE_SYNC_IDLE, FILE_SYNC_STARTING, FILE_SYNC_STOPPED,
37 FILE_SYNC_SYNCING,38 FILE_SYNC_SYNCING,
38 MSG_KEY, STATUS_KEY,39 MSG_KEY, STATUS_KEY,
@@ -77,7 +78,7 @@
77 return result78 return result
7879
7980
80def transform_failure(f):81def transform_failure(f, auth_error=None):
81 """Decorator to apply to DBus error signals.82 """Decorator to apply to DBus error signals.
8283
83 With this call, a Failure is transformed into a string-string dict.84 With this call, a Failure is transformed into a string-string dict.
@@ -85,8 +86,11 @@
85 """86 """
86 def inner(error, _=None):87 def inner(error, _=None):
87 """Do the Failure transformation."""88 """Do the Failure transformation."""
89 logger.error('processing failure: %r', error.printTraceback())
88 error_dict = error_handler(error)90 error_dict = error_handler(error)
89 if _ is not None:91 if auth_error is not None and error.check(UnauthorizedError):
92 result = auth_error(error_dict)
93 elif _ is not None:
90 result = f(_, error_dict)94 result = f(_, error_dict)
91 else:95 else:
92 result = f(error_dict)96 result = f(error_dict)
@@ -115,19 +119,25 @@
115 super(ControlPanelBackend, self).__init__(*args, **kwargs)119 super(ControlPanelBackend, self).__init__(*args, **kwargs)
116 self.backend = backend120 self.backend = backend
117 self.backend.status_changed_handler = self.process_status121 self.backend.status_changed_handler = self.process_status
122 self.transform = lambda f: transform_failure(f, self.UnauthorizedError)
118 logger.debug('ControlPanelBackend: created with %r, %r.\n'123 logger.debug('ControlPanelBackend: created with %r, %r.\n'
119 'status_changed_handler is %r.',124 'status_changed_handler is %r.',
120 args, kwargs, self.process_status)125 args, kwargs, self.process_status)
121126
122 # pylint: disable=C0103127 # pylint: disable=C0103
123128
129 @log_call(logger.error)
130 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
131 def UnauthorizedError(self, error):
132 """The credentials are not valid."""
133
124 @log_call(logger.debug)134 @log_call(logger.debug)
125 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")135 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
126 def account_info(self):136 def account_info(self):
127 """Find out the account info for the current logged in user."""137 """Find out the account info for the current logged in user."""
128 d = self.backend.account_info()138 d = self.backend.account_info()
129 d.addCallback(self.AccountInfoReady)139 d.addCallback(self.AccountInfoReady)
130 d.addErrback(transform_failure(self.AccountInfoError))140 d.addErrback(self.transform(self.AccountInfoError))
131141
132 @log_call(logger.debug)142 @log_call(logger.debug)
133 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")143 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
@@ -147,12 +157,13 @@
147 """Find out the devices info for the logged in user."""157 """Find out the devices info for the logged in user."""
148 d = self.backend.devices_info()158 d = self.backend.devices_info()
149 d.addCallback(self.DevicesInfoReady)159 d.addCallback(self.DevicesInfoReady)
150 d.addErrback(transform_failure(self.DevicesInfoError))160 d.addErrback(self.transform(self.DevicesInfoError))
151161
152 @log_call(logger.debug)
153 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")162 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")
154 def DevicesInfoReady(self, info):163 def DevicesInfoReady(self, info):
155 """The info for the devices is available right now."""164 """The info for the devices is available right now."""
165 logger.debug('DevicesInfoReady: args %r',
166 filter_field(info, field='device_id'))
156167
157 @log_call(logger.error)168 @log_call(logger.error)
158 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")169 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
@@ -161,41 +172,41 @@
161172
162 #---173 #---
163174
164 @log_call(logger.info)175 @log_call(logger.info, with_args=False)
165 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="sa{ss}")176 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="sa{ss}")
166 def change_device_settings(self, device_id, settings):177 def change_device_settings(self, device_id, settings):
167 """Configure a given device."""178 """Configure a given device."""
168 d = self.backend.change_device_settings(device_id, settings)179 d = self.backend.change_device_settings(device_id, settings)
169 d.addCallback(self.DeviceSettingsChanged)180 d.addCallback(self.DeviceSettingsChanged)
170 d.addErrback(transform_failure(self.DeviceSettingsChangeError),181 d.addErrback(self.transform(self.DeviceSettingsChangeError),
171 device_id)182 device_id)
172183
173 @log_call(logger.info)184 @log_call(logger.info, with_args=False)
174 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")185 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
175 def DeviceSettingsChanged(self, device_id):186 def DeviceSettingsChanged(self, device_id):
176 """The settings for the device were changed."""187 """The settings for the device were changed."""
177188
178 @log_call(logger.error)189 @log_call(logger.error, with_args=False)
179 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")190 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")
180 def DeviceSettingsChangeError(self, device_id, error):191 def DeviceSettingsChangeError(self, device_id, error):
181 """Problem changing settings for the device."""192 """Problem changing settings for the device."""
182193
183 #---194 #---
184195
185 @log_call(logger.warning)196 @log_call(logger.warning, with_args=False)
186 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="s")197 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="s")
187 def remove_device(self, device_id):198 def remove_device(self, device_id):
188 """Remove a given device."""199 """Remove a given device."""
189 d = self.backend.remove_device(device_id)200 d = self.backend.remove_device(device_id)
190 d.addCallback(self.DeviceRemoved)201 d.addCallback(self.DeviceRemoved)
191 d.addErrback(transform_failure(self.DeviceRemovalError), device_id)202 d.addErrback(self.transform(self.DeviceRemovalError), device_id)
192203
193 @log_call(logger.warning)204 @log_call(logger.warning, with_args=False)
194 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")205 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
195 def DeviceRemoved(self, device_id):206 def DeviceRemoved(self, device_id):
196 """The removal for the device was completed."""207 """The removal for the device was completed."""
197208
198 @log_call(logger.error)209 @log_call(logger.error, with_args=False)
199 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")210 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")
200 def DeviceRemovalError(self, device_id, error):211 def DeviceRemovalError(self, device_id, error):
201 """Problem removing the device."""212 """Problem removing the device."""
@@ -234,7 +245,7 @@
234 """Get the status of the file sync service."""245 """Get the status of the file sync service."""
235 d = self.backend.file_sync_status()246 d = self.backend.file_sync_status()
236 d.addCallback(self.process_status)247 d.addCallback(self.process_status)
237 d.addErrback(transform_failure(self.FileSyncStatusError))248 d.addErrback(self.transform(self.FileSyncStatusError))
238249
239 @log_call(logger.debug)250 @log_call(logger.debug)
240 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")251 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
@@ -284,7 +295,7 @@
284 """Enable the files service."""295 """Enable the files service."""
285 d = self.backend.enable_files()296 d = self.backend.enable_files()
286 d.addCallback(lambda _: self.FilesEnabled())297 d.addCallback(lambda _: self.FilesEnabled())
287 d.addErrback(transform_failure(self.FilesEnableError))298 d.addErrback(self.transform(self.FilesEnableError))
288299
289 @log_call(logger.debug)300 @log_call(logger.debug)
290 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)301 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
@@ -304,7 +315,7 @@
304 """Disable the files service."""315 """Disable the files service."""
305 d = self.backend.disable_files()316 d = self.backend.disable_files()
306 d.addCallback(lambda _: self.FilesDisabled())317 d.addCallback(lambda _: self.FilesDisabled())
307 d.addErrback(transform_failure(self.FilesDisableError))318 d.addErrback(self.transform(self.FilesDisableError))
308319
309 @log_call(logger.debug)320 @log_call(logger.debug)
310 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)321 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
@@ -324,7 +335,7 @@
324 """Connect the files service."""335 """Connect the files service."""
325 d = self.backend.connect_files()336 d = self.backend.connect_files()
326 d.addCallback(lambda _: self.FilesConnected())337 d.addCallback(lambda _: self.FilesConnected())
327 d.addErrback(transform_failure(self.FilesConnectError))338 d.addErrback(self.transform(self.FilesConnectError))
328339
329 @log_call(logger.debug)340 @log_call(logger.debug)
330 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)341 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
@@ -344,7 +355,7 @@
344 """Disconnect the files service."""355 """Disconnect the files service."""
345 d = self.backend.disconnect_files()356 d = self.backend.disconnect_files()
346 d.addCallback(lambda _: self.FilesDisconnected())357 d.addCallback(lambda _: self.FilesDisconnected())
347 d.addErrback(transform_failure(self.FilesDisconnectError))358 d.addErrback(self.transform(self.FilesDisconnectError))
348359
349 @log_call(logger.debug)360 @log_call(logger.debug)
350 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)361 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
@@ -364,7 +375,7 @@
364 """Restart the files service."""375 """Restart the files service."""
365 d = self.backend.restart_files()376 d = self.backend.restart_files()
366 d.addCallback(lambda _: self.FilesRestarted())377 d.addCallback(lambda _: self.FilesRestarted())
367 d.addErrback(transform_failure(self.FilesRestartError))378 d.addErrback(self.transform(self.FilesRestartError))
368379
369 @log_call(logger.debug)380 @log_call(logger.debug)
370 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)381 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
@@ -384,7 +395,7 @@
384 """Start the files service."""395 """Start the files service."""
385 d = self.backend.start_files()396 d = self.backend.start_files()
386 d.addCallback(lambda _: self.FilesStarted())397 d.addCallback(lambda _: self.FilesStarted())
387 d.addErrback(transform_failure(self.FilesStartError))398 d.addErrback(self.transform(self.FilesStartError))
388399
389 @log_call(logger.debug)400 @log_call(logger.debug)
390 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)401 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
@@ -404,7 +415,7 @@
404 """Stop the files service."""415 """Stop the files service."""
405 d = self.backend.stop_files()416 d = self.backend.stop_files()
406 d.addCallback(lambda _: self.FilesStopped())417 d.addCallback(lambda _: self.FilesStopped())
407 d.addErrback(transform_failure(self.FilesStopError))418 d.addErrback(self.transform(self.FilesStopError))
408419
409 @log_call(logger.debug)420 @log_call(logger.debug)
410 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)421 @signal(dbus_interface=DBUS_PREFERENCES_IFACE)
@@ -424,7 +435,7 @@
424 """Find out the volumes info for the logged in user."""435 """Find out the volumes info for the logged in user."""
425 d = self.backend.volumes_info()436 d = self.backend.volumes_info()
426 d.addCallback(self.VolumesInfoReady)437 d.addCallback(self.VolumesInfoReady)
427 d.addErrback(transform_failure(self.VolumesInfoError))438 d.addErrback(self.transform(self.VolumesInfoError))
428439
429 @log_call(logger.debug)440 @log_call(logger.debug)
430 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a(ssaa{ss})")441 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a(ssaa{ss})")
@@ -444,7 +455,7 @@
444 """Configure a given volume."""455 """Configure a given volume."""
445 d = self.backend.change_volume_settings(volume_id, settings)456 d = self.backend.change_volume_settings(volume_id, settings)
446 d.addCallback(self.VolumeSettingsChanged)457 d.addCallback(self.VolumeSettingsChanged)
447 d.addErrback(transform_failure(self.VolumeSettingsChangeError),458 d.addErrback(self.transform(self.VolumeSettingsChangeError),
448 volume_id)459 volume_id)
449460
450 @log_call(logger.info)461 @log_call(logger.info)
@@ -465,7 +476,7 @@
465 """Return the replications info."""476 """Return the replications info."""
466 d = self.backend.replications_info()477 d = self.backend.replications_info()
467 d.addCallback(self.ReplicationsInfoReady)478 d.addCallback(self.ReplicationsInfoReady)
468 d.addErrback(transform_failure(self.ReplicationsInfoError))479 d.addErrback(self.transform(self.ReplicationsInfoError))
469480
470 @log_call(logger.debug)481 @log_call(logger.debug)
471 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")482 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")
@@ -485,7 +496,7 @@
485 """Configure a given replication."""496 """Configure a given replication."""
486 d = self.backend.change_replication_settings(replication_id, settings)497 d = self.backend.change_replication_settings(replication_id, settings)
487 d.addCallback(self.ReplicationSettingsChanged)498 d.addCallback(self.ReplicationSettingsChanged)
488 d.addErrback(transform_failure(self.ReplicationSettingsChangeError),499 d.addErrback(self.transform(self.ReplicationSettingsChangeError),
489 replication_id)500 replication_id)
490501
491 @log_call(logger.info)502 @log_call(logger.info)
@@ -506,7 +517,7 @@
506 """Check if the extension to sync bookmarks is installed."""517 """Check if the extension to sync bookmarks is installed."""
507 d = self.backend.query_bookmark_extension()518 d = self.backend.query_bookmark_extension()
508 d.addCallback(self.QueryBookmarksResult)519 d.addCallback(self.QueryBookmarksResult)
509 d.addErrback(transform_failure(self.QueryBookmarksError))520 d.addErrback(self.transform(self.QueryBookmarksError))
510521
511 @log_call(logger.debug)522 @log_call(logger.debug)
512 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="b")523 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="b")
@@ -526,7 +537,7 @@
526 """Install the extension to sync bookmarks."""537 """Install the extension to sync bookmarks."""
527 d = self.backend.install_bookmarks_extension()538 d = self.backend.install_bookmarks_extension()
528 d.addCallback(lambda _: self.InstallBookmarksSuccess())539 d.addCallback(lambda _: self.InstallBookmarksSuccess())
529 d.addErrback(transform_failure(self.InstallBookmarksError))540 d.addErrback(self.transform(self.InstallBookmarksError))
530541
531 @log_call(logger.info)542 @log_call(logger.info)
532 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="")543 @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="")
@@ -538,15 +549,24 @@
538 def InstallBookmarksError(self, error):549 def InstallBookmarksError(self, error):
539 """Problem installing the extension to sync bookmarks."""550 """Problem installing the extension to sync bookmarks."""
540551
552 #---
553
554 @log_call(logger.info)
555 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
556 def shutdown(self):
557 """Shutdown this service."""
558 self.backend.shutdown()
559
541560
542def init_mainloop():561def init_mainloop():
543 """Start the DBus mainloop."""562 """Start the DBus mainloop."""
544 DBusGMainLoop(set_as_default=True)563 DBusGMainLoop(set_as_default=True)
545564
546565
547def run_mainloop():566def run_mainloop(loop=None):
548 """Run the gobject main loop."""567 """Run the gobject main loop."""
549 loop = gobject.MainLoop()568 if loop is None:
569 loop = gobject.MainLoop()
550 loop.run()570 loop.run()
551571
552572
@@ -566,10 +586,10 @@
566 return dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())586 return dbus.service.BusName(DBUS_BUS_NAME, bus=dbus.SessionBus())
567587
568588
569def publish_backend(backend=None):589def publish_backend(backend=None, shutdown_func=None):
570 """Publish the backend on the DBus."""590 """Publish the backend on the DBus."""
571 if backend is None:591 if backend is None:
572 backend = ControlBackend()592 backend = ControlBackend(shutdown_func=shutdown_func)
573 return ControlPanelBackend(backend=backend,593 return ControlPanelBackend(backend=backend,
574 object_path=DBUS_PREFERENCES_PATH,594 object_path=DBUS_PREFERENCES_PATH,
575 bus_name=get_busname())595 bus_name=get_busname())
@@ -579,7 +599,8 @@
579 """Hook the DBus listeners and start the main loop."""599 """Hook the DBus listeners and start the main loop."""
580 init_mainloop()600 init_mainloop()
581 if register_service():601 if register_service():
582 publish_backend()602 loop = gobject.MainLoop()
583 run_mainloop()603 publish_backend(shutdown_func=loop.quit)
604 run_mainloop(loop=loop)
584 else:605 else:
585 print "Control panel backend already running."606 print "Control panel backend already running."
586607
=== modified file 'ubuntuone/controlpanel/gtk/__init__.py'
--- ubuntuone/controlpanel/gtk/__init__.py 2011-03-23 16:06:41 +0000
+++ ubuntuone/controlpanel/gtk/__init__.py 2011-04-08 19:43:13 +0000
@@ -18,5 +18,7 @@
1818
19"""The GTK graphical interface for the control panel for Ubuntu One."""19"""The GTK graphical interface for the control panel for Ubuntu One."""
2020
21DBUS_BUS_NAME = "com.ubuntuone.controlpanel.gui"21DBUS_BUS_NAME = 'com.ubuntuone.controlpanel.gui'
22TRANSLATION_DOMAIN = "ubuntuone-control-panel"22DBUS_PATH = '/gui'
23DBUS_IFACE_GUI = 'com.ubuntuone.controlpanel.gui'
24TRANSLATION_DOMAIN = 'ubuntuone-control-panel'
2325
=== modified file 'ubuntuone/controlpanel/gtk/gui.py'
--- ubuntuone/controlpanel/gtk/gui.py 2011-03-23 16:06:41 +0000
+++ ubuntuone/controlpanel/gtk/gui.py 2011-04-08 19:43:13 +0000
@@ -1,6 +1,7 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
22
3# Authors: Natalia B Bidart <natalia.bidart@canonical.com>3# Authors: Natalia B Bidart <natalia.bidart@canonical.com>
4# Eric Casteleijn <eric.casteleijn@canonical.com>
4#5#
5# Copyright 2010 Canonical Ltd.6# Copyright 2010 Canonical Ltd.
6#7#
@@ -31,6 +32,7 @@
31import gobject32import gobject
32import ubuntu_sso33import ubuntu_sso
3334
35from dbus.mainloop.glib import DBusGMainLoop
34from ubuntu_sso import networkstate36from ubuntu_sso import networkstate
35from ubuntu_sso.credentials import (TC_URL_KEY, HELP_TEXT_KEY, WINDOW_ID_KEY,37from ubuntu_sso.credentials import (TC_URL_KEY, HELP_TEXT_KEY, WINDOW_ID_KEY,
36 PING_URL_KEY)38 PING_URL_KEY)
@@ -41,6 +43,9 @@
41 PING_URL as U1_PING_URL, DESCRIPTION as U1_DESCRIPTION)43 PING_URL as U1_PING_URL, DESCRIPTION as U1_DESCRIPTION)
42# pylint: enable=E0611,F040144# pylint: enable=E0611,F0401
4345
46from ubuntuone.controlpanel.gtk import (
47 DBUS_IFACE_GUI, DBUS_BUS_NAME as DBUS_BUS_NAME_GUI,
48 DBUS_PATH as DBUS_PATH_GUI)
44from ubuntuone.controlpanel.gtk.widgets import LabelLoading, PanelTitle49from ubuntuone.controlpanel.gtk.widgets import LabelLoading, PanelTitle
45# Use ubiquity package when ready (LP: #673665)50# Use ubiquity package when ready (LP: #673665)
46from ubuntuone.controlpanel.gtk.widgets import GreyableBin51from ubuntuone.controlpanel.gtk.widgets import GreyableBin
@@ -50,10 +55,18 @@
50from ubuntuone.controlpanel.backend import (DEVICE_TYPE_PHONE,55from ubuntuone.controlpanel.backend import (DEVICE_TYPE_PHONE,
51 DEVICE_TYPE_COMPUTER, bool_str)56 DEVICE_TYPE_COMPUTER, bool_str)
52from ubuntuone.controlpanel.logger import setup_logging, log_call57from ubuntuone.controlpanel.logger import setup_logging, log_call
53from ubuntuone.controlpanel.utils import get_data_file58from ubuntuone.controlpanel.utils import (get_data_file,
59 ERROR_TYPE, ERROR_MESSAGE)
5460
55from ubuntuone.controlpanel.gtk import package_manager, TRANSLATION_DOMAIN61from ubuntuone.controlpanel.gtk import package_manager, TRANSLATION_DOMAIN
5662
63try:
64 from gi.repository import Unity # pylint: disable=E0611
65 USE_LIBUNITY = True
66 U1_DOTDESKTOP = "ubuntuone-control-panel-gtk.desktop"
67except ImportError:
68 USE_LIBUNITY = False
69
57logger = setup_logging('gtk.gui')70logger = setup_logging('gtk.gui')
58_ = gettext.gettext71_ = gettext.gettext
5972
@@ -63,6 +76,7 @@
63ERROR_COLOR = 'red'76ERROR_COLOR = 'red'
64LOADING = _('Loading...')77LOADING = _('Loading...')
65VALUE_ERROR = _('Value could not be retrieved.')78VALUE_ERROR = _('Value could not be retrieved.')
79UNKNOWN_ERROR = _('Unknown error')
66WARNING_MARKUP = '<span foreground="%s"><b>%%s</b></span>' % ERROR_COLOR80WARNING_MARKUP = '<span foreground="%s"><b>%%s</b></span>' % ERROR_COLOR
67KILOBYTES = 102481KILOBYTES = 1024
68NO_OP = lambda *a, **kw: None82NO_OP = lambda *a, **kw: None
@@ -74,6 +88,49 @@
74 logger.error('Error handler received: %r, %r', args, kwargs)88 logger.error('Error handler received: %r, %r', args, kwargs)
7589
7690
91def register_service(bus):
92 """Try to register DBus service for making sure we run only one instance.
93
94 Return True if succesfully registered, False if already running.
95 """
96 name = bus.request_name(DBUS_BUS_NAME_GUI,
97 dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
98 return name != dbus.bus.REQUEST_NAME_REPLY_EXISTS
99
100
101def publish_service(window=None, switch_to='', alert=False):
102 """Publish the service on DBus."""
103 if window is None:
104 window = ControlPanelWindow(switch_to=switch_to, alert=alert)
105 return ControlPanelService(window)
106
107
108def main(switch_to='', alert=False):
109 """Hook the DBus listeners and start the main loop."""
110 DBusGMainLoop(set_as_default=True)
111 bus = dbus.SessionBus()
112 if register_service(bus):
113 publish_service(switch_to=switch_to, alert=alert)
114 else:
115 obj = bus.get_object(DBUS_BUS_NAME_GUI, DBUS_PATH_GUI)
116 service = dbus.Interface(obj, dbus_interface=DBUS_IFACE_GUI)
117
118 def gui_error_handler(*args, **kwargs):
119 """Log errors when calling D-Bus methods in a async way."""
120 logger.error('Error handler received: %r, %r', args, kwargs)
121 gtk.main_quit()
122
123 def gui_reply_handler(*args, **kwargs):
124 """Exit when done."""
125 gtk.main_quit()
126
127 service.switch_to_alert(
128 switch_to, alert, reply_handler=gui_reply_handler,
129 error_handler=gui_error_handler)
130
131 gtk.main()
132
133
77def filter_by_app_name(f):134def filter_by_app_name(f):
78 """Excecute 'f' filtering by app_name."""135 """Excecute 'f' filtering by app_name."""
79136
@@ -192,10 +249,19 @@
192 self.message.set_markup(message)249 self.message.set_markup(message)
193250
194 @log_call(logger.error)251 @log_call(logger.error)
195 def on_error(self, message=None):252 def on_error(self, message=None, error_dict=None):
196 """Use this callback to stop the Loading and set a warning message."""253 """Use this callback to stop the Loading and set a warning message."""
197 if message == None:254 if message is None and error_dict is None:
198 message = VALUE_ERROR255 message = VALUE_ERROR
256 elif message is None and error_dict is not None:
257 error_type = error_dict.get(ERROR_TYPE, UNKNOWN_ERROR)
258 error_msg = error_dict.get(ERROR_MESSAGE)
259 if error_msg:
260 message = "%s (%s: %s)" % (VALUE_ERROR, error_type, error_msg)
261 else:
262 message = "%s (%s)" % (VALUE_ERROR, error_type)
263
264 assert message is not None
199265
200 self.message.stop()266 self.message.stop()
201 self.message.set_markup(WARNING_MARKUP % message)267 self.message.set_markup(WARNING_MARKUP % message)
@@ -492,6 +558,10 @@
492 elif name == self.MUSIC_DISPLAY_NAME:558 elif name == self.MUSIC_DISPLAY_NAME:
493 icon_name = self.MUSIC_ICON_NAME559 icon_name = self.MUSIC_ICON_NAME
494560
561 if volume[u'path'] is None:
562 logger.warning('on_volumes_info_ready: about to store a '
563 'volume with None path: %r', volume)
564
495 row = (name, bool(volume[u'subscribed']), icon_name, True,565 row = (name, bool(volume[u'subscribed']), icon_name, True,
496 sensitive, gtk.ICON_SIZE_MENU, volume['volume_id'],566 sensitive, gtk.ICON_SIZE_MENU, volume['volume_id'],
497 volume[u'path'])567 volume[u'path'])
@@ -509,7 +579,7 @@
509 @log_call(logger.error)579 @log_call(logger.error)
510 def on_volumes_info_error(self, error_dict=None):580 def on_volumes_info_error(self, error_dict=None):
511 """Backend notifies of an error when fetching volumes info."""581 """Backend notifies of an error when fetching volumes info."""
512 self.on_error()582 self.on_error(error_dict=error_dict)
513583
514 @log_call(logger.info)584 @log_call(logger.info)
515 def on_volume_settings_changed(self, volume_id):585 def on_volume_settings_changed(self, volume_id):
@@ -548,7 +618,14 @@
548 """The user double clicked on a row."""618 """The user double clicked on a row."""
549 treeiter = self.volumes_store.get_iter(path)619 treeiter = self.volumes_store.get_iter(path)
550 volume_path = self.volumes_store.get_value(treeiter, 7)620 volume_path = self.volumes_store.get_value(treeiter, 7)
551 uri_hook(None, FILE_URI_PREFIX + volume_path)621 if volume_path is None:
622 logger.warning('on_volumes_view_row_activated: volume_path for '
623 'tree_path %r is None', path)
624 elif not os.path.exists(volume_path):
625 logger.warning('on_volumes_view_row_activated: path %r '
626 'does not exist', volume_path)
627 else:
628 uri_hook(None, FILE_URI_PREFIX + volume_path)
552629
553 def load(self):630 def load(self):
554 """Load the volume list."""631 """Load the volume list."""
@@ -827,7 +904,7 @@
827 @log_call(logger.error)904 @log_call(logger.error)
828 def on_devices_info_error(self, error_dict=None):905 def on_devices_info_error(self, error_dict=None):
829 """Backend notifies of an error when fetching volumes info."""906 """Backend notifies of an error when fetching volumes info."""
830 self.on_error()907 self.on_error(error_dict=error_dict)
831 self.is_processing = False908 self.is_processing = False
832909
833 @log_call(logger.warning)910 @log_call(logger.warning)
@@ -992,7 +1069,8 @@
992 def on_file_sync_status_changed(self, status):1069 def on_file_sync_status_changed(self, status):
993 """File Sync status changed."""1070 """File Sync status changed."""
994 enabled = status != backend.FILE_SYNC_DISABLED1071 enabled = status != backend.FILE_SYNC_DISABLED
995 logger.info('FileSyncService: enabled? %r', enabled)1072 logger.info('FileSyncService: on_file_sync_status_changed: '
1073 'status %r, enabled? %r', status, enabled)
996 self.check_button.set_active(enabled)1074 self.check_button.set_active(enabled)
997 # if service is disabled, disable the action_button1075 # if service is disabled, disable the action_button
998 self.action_button.set_sensitive(enabled)1076 self.action_button.set_sensitive(enabled)
@@ -1025,8 +1103,8 @@
1025class DesktopcouchService(Service):1103class DesktopcouchService(Service):
1026 """A desktopcouch service."""1104 """A desktopcouch service."""
10271105
1028 INSTALL_PACKAGE = _('Install the %(plugin_name)s '1106 INSTALL_PACKAGE = _('Install the %(plugin_name)s for the sync service: '
1029 'for %(service_name)s sync')1107 '%(service_name)s')
10301108
1031 def __init__(self, service_id, name, enabled,1109 def __init__(self, service_id, name, enabled,
1032 container, check_button,1110 container, check_button,
@@ -1152,6 +1230,7 @@
1152 @log_call(logger.debug)1230 @log_call(logger.debug)
1153 def load(self):1231 def load(self):
1154 """Load info."""1232 """Load info."""
1233 self.replications.hide()
1155 if self.install_box is not None:1234 if self.install_box is not None:
1156 self.itself.remove(self.install_box)1235 self.itself.remove(self.install_box)
1157 self.install_box = None1236 self.install_box = None
@@ -1159,7 +1238,6 @@
1159 logger.info('load: has_desktopcouch? %r', self.has_desktopcouch)1238 logger.info('load: has_desktopcouch? %r', self.has_desktopcouch)
1160 if not self.has_desktopcouch:1239 if not self.has_desktopcouch:
1161 self.message.set_text('')1240 self.message.set_text('')
1162 self.replications.hide()
11631241
1164 self.install_box = InstallPackage(self.DESKTOPCOUCH_PKG)1242 self.install_box = InstallPackage(self.DESKTOPCOUCH_PKG)
1165 self.install_box.connect('finished', self.load_replications)1243 self.install_box.connect('finished', self.load_replications)
@@ -1211,7 +1289,7 @@
1211 error_dict.get('error_type', None) == 'NoPairingRecord':1289 error_dict.get('error_type', None) == 'NoPairingRecord':
1212 self.on_error(self.NO_PAIRING_RECORD)1290 self.on_error(self.NO_PAIRING_RECORD)
1213 else:1291 else:
1214 self.on_error()1292 self.on_error(error_dict=error_dict)
12151293
12161294
1217class FileSyncStatus(gtk.HBox, ControlPanelMixin):1295class FileSyncStatus(gtk.HBox, ControlPanelMixin):
@@ -1252,6 +1330,8 @@
1252 self.button.connect('clicked', self._on_button_clicked)1330 self.button.connect('clicked', self._on_button_clicked)
1253 self.pack_start(self.button, expand=False)1331 self.pack_start(self.button, expand=False)
12541332
1333 self.show_all()
1334
1255 self.backend.connect_to_signal('FileSyncStatusDisabled',1335 self.backend.connect_to_signal('FileSyncStatusDisabled',
1256 self.on_file_sync_status_disabled)1336 self.on_file_sync_status_disabled)
1257 self.backend.connect_to_signal('FileSyncStatusStarting',1337 self.backend.connect_to_signal('FileSyncStatusStarting',
@@ -1268,10 +1348,10 @@
1268 self.on_file_sync_status_error)1348 self.on_file_sync_status_error)
1269 self.backend.connect_to_signal('FilesStartError',1349 self.backend.connect_to_signal('FilesStartError',
1270 self.on_files_start_error)1350 self.on_files_start_error)
12711351 self.backend.connect_to_signal('FilesEnabled',
1272 self.backend.file_sync_status(reply_handler=NO_OP,1352 self.on_file_sync_status_starting)
1273 error_handler=error_handler)1353 self.backend.connect_to_signal('FilesDisabled',
1274 self.show_all()1354 self.on_file_sync_status_disabled)
12751355
1276 def _update_status(self, msg, action, callback,1356 def _update_status(self, msg, action, callback,
1277 icon=None, color=None, tooltip=None):1357 icon=None, color=None, tooltip=None):
@@ -1296,42 +1376,42 @@
1296 button.get_data('callback')(button)1376 button.get_data('callback')(button)
12971377
1298 @log_call(logger.info)1378 @log_call(logger.info)
1299 def on_file_sync_status_disabled(self, msg):1379 def on_file_sync_status_disabled(self, msg=None):
1300 """Backend notifies of file sync status being disabled."""1380 """Backend notifies of file sync status being disabled."""
1301 self._update_status(self.FILE_SYNC_DISABLED,1381 self._update_status(self.FILE_SYNC_DISABLED,
1302 self.ENABLE, self.on_enable_clicked,1382 self.ENABLE, self.on_enable_clicked,
1303 '✘', 'red', self.ENABLE_TOOLTIP)1383 '✘', 'red', self.ENABLE_TOOLTIP)
13041384
1305 @log_call(logger.info)1385 @log_call(logger.info)
1306 def on_file_sync_status_starting(self, msg):1386 def on_file_sync_status_starting(self, msg=None):
1307 """Backend notifies of file sync status being starting."""1387 """Backend notifies of file sync status being starting."""
1308 self._update_status(self.FILE_SYNC_STARTING,1388 self._update_status(self.FILE_SYNC_STARTING,
1309 self.STOP, self.on_stop_clicked,1389 self.STOP, self.on_stop_clicked,
1310 '⇅', ORANGE, self.STOP_TOOLTIP)1390 '⇅', ORANGE, self.STOP_TOOLTIP)
13111391
1312 @log_call(logger.info)1392 @log_call(logger.info)
1313 def on_file_sync_status_stopped(self, msg):1393 def on_file_sync_status_stopped(self, msg=None):
1314 """Backend notifies of file sync being stopped."""1394 """Backend notifies of file sync being stopped."""
1315 self._update_status(self.FILE_SYNC_STOPPED,1395 self._update_status(self.FILE_SYNC_STOPPED,
1316 self.START, self.on_start_clicked,1396 self.START, self.on_start_clicked,
1317 '✘', 'red', self.START_TOOLTIP)1397 '✘', 'red', self.START_TOOLTIP)
13181398
1319 @log_call(logger.info)1399 @log_call(logger.info)
1320 def on_file_sync_status_disconnected(self, msg):1400 def on_file_sync_status_disconnected(self, msg=None):
1321 """Backend notifies of file sync status being ready."""1401 """Backend notifies of file sync status being ready."""
1322 self._update_status(self.FILE_SYNC_DISCONNECTED,1402 self._update_status(self.FILE_SYNC_DISCONNECTED,
1323 self.CONNECT, self.on_connect_clicked,1403 self.CONNECT, self.on_connect_clicked,
1324 '✘', 'red', self.CONNECT_TOOLTIP,)1404 '✘', 'red', self.CONNECT_TOOLTIP,)
13251405
1326 @log_call(logger.info)1406 @log_call(logger.info)
1327 def on_file_sync_status_syncing(self, msg):1407 def on_file_sync_status_syncing(self, msg=None):
1328 """Backend notifies of file sync status being syncing."""1408 """Backend notifies of file sync status being syncing."""
1329 self._update_status(self.FILE_SYNC_SYNCING,1409 self._update_status(self.FILE_SYNC_SYNCING,
1330 self.DISCONNECT, self.on_disconnect_clicked,1410 self.DISCONNECT, self.on_disconnect_clicked,
1331 '⇅', ORANGE, self.DISCONNECT_TOOLTIP)1411 '⇅', ORANGE, self.DISCONNECT_TOOLTIP)
13321412
1333 @log_call(logger.info)1413 @log_call(logger.info)
1334 def on_file_sync_status_idle(self, msg):1414 def on_file_sync_status_idle(self, msg=None):
1335 """Backend notifies of file sync status being idle."""1415 """Backend notifies of file sync status being idle."""
1336 self._update_status(self.FILE_SYNC_IDLE,1416 self._update_status(self.FILE_SYNC_IDLE,
1337 self.DISCONNECT, self.on_disconnect_clicked,1417 self.DISCONNECT, self.on_disconnect_clicked,
@@ -1385,6 +1465,11 @@
1385 self.backend.stop_files(reply_handler=NO_OP,1465 self.backend.stop_files(reply_handler=NO_OP,
1386 error_handler=error_handler)1466 error_handler=error_handler)
13871467
1468 def load(self):
1469 """Load the information."""
1470 self.backend.file_sync_status(reply_handler=NO_OP,
1471 error_handler=error_handler)
1472
13881473
1389class ManagementPanel(gtk.VBox, ControlPanelMixin):1474class ManagementPanel(gtk.VBox, ControlPanelMixin):
1390 """The management panel.1475 """The management panel.
@@ -1396,6 +1481,7 @@
1396 __gsignals__ = {1481 __gsignals__ = {
1397 'local-device-removed': (gobject.SIGNAL_RUN_FIRST,1482 'local-device-removed': (gobject.SIGNAL_RUN_FIRST,
1398 gobject.TYPE_NONE, ()),1483 gobject.TYPE_NONE, ()),
1484 'unauthorized': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
1399 }1485 }
14001486
1401 QUOTA_LABEL = _('Using %(used)s of %(total)s (%(percentage).0f%%)')1487 QUOTA_LABEL = _('Using %(used)s of %(total)s (%(percentage).0f%%)')
@@ -1421,6 +1507,8 @@
1421 self.on_account_info_ready)1507 self.on_account_info_ready)
1422 self.backend.connect_to_signal('AccountInfoError',1508 self.backend.connect_to_signal('AccountInfoError',
1423 self.on_account_info_error)1509 self.on_account_info_error)
1510 self.backend.connect_to_signal('UnauthorizedError',
1511 self.on_unauthorized_error)
14241512
1425 self.quota_progressbar.set_sensitive(False)1513 self.quota_progressbar.set_sensitive(False)
14261514
@@ -1461,7 +1549,11 @@
14611549
1462 self.services_button.set_name(self.SERVICES_BUTTON_NAME)1550 self.services_button.set_name(self.SERVICES_BUTTON_NAME)
1463 self.services_button.set_tooltip_text(self.SERVICES_BUTTON_TOOLTIP)1551 self.services_button.set_tooltip_text(self.SERVICES_BUTTON_TOOLTIP)
1464 self.services.load()1552
1553 self.enable_volumes = lambda: self.volumes_button.set_sensitive(True)
1554 self.disable_volumes = lambda: self.volumes_button.set_sensitive(False)
1555 self.backend.connect_to_signal('FilesEnabled', self.enable_volumes)
1556 self.backend.connect_to_signal('FilesDisabled', self.disable_volumes)
14651557
1466 def _update_quota(self, msg, data=None):1558 def _update_quota(self, msg, data=None):
1467 """Update the quota info."""1559 """Update the quota info."""
@@ -1491,6 +1583,8 @@
1491 """Load the account info and file sync status list."""1583 """Load the account info and file sync status list."""
1492 self.backend.account_info(reply_handler=NO_OP,1584 self.backend.account_info(reply_handler=NO_OP,
1493 error_handler=error_handler)1585 error_handler=error_handler)
1586 self.status_label.load()
1587 self.services.load()
14941588
1495 @log_call(logger.debug)1589 @log_call(logger.debug)
1496 def on_account_info_ready(self, info):1590 def on_account_info_ready(self, info):
@@ -1506,15 +1600,22 @@
1506 """Backend notifies of an error when fetching account info."""1600 """Backend notifies of an error when fetching account info."""
1507 self._update_quota(msg='')1601 self._update_quota(msg='')
15081602
15091603 @log_call(logger.error)
1510class ControlPanel(gtk.Notebook):1604 def on_unauthorized_error(self, error_dict=None):
1605 """Backend notifies that credentials are not valid."""
1606 self.emit('unauthorized')
1607
1608
1609class ControlPanel(gtk.Notebook, ControlPanelMixin):
1511 """The control panel per se, can be added into any other widget."""1610 """The control panel per se, can be added into any other widget."""
15121611
1513 # should not be any larger than 736x5251612 # should not be any larger than 736x525
15141613
1515 def __init__(self, main_window):1614 def __init__(self, main_window):
1516 gtk.Notebook.__init__(self)1615 gtk.Notebook.__init__(self)
1616 ControlPanelMixin.__init__(self)
1517 gtk.link_button_set_uri_hook(uri_hook)1617 gtk.link_button_set_uri_hook(uri_hook)
1618 self.connect('destroy', self.shutdown)
15181619
1519 self.main_window = main_window1620 self.main_window = main_window
15201621
@@ -1531,6 +1632,8 @@
1531 self.on_show_management_panel)1632 self.on_show_management_panel)
1532 self.management.connect('local-device-removed',1633 self.management.connect('local-device-removed',
1533 self.on_show_overview_panel)1634 self.on_show_overview_panel)
1635 self.management.connect('unauthorized',
1636 self.on_show_overview_panel)
15341637
1535 self.show()1638 self.show()
1536 self.on_show_overview_panel()1639 self.on_show_overview_panel()
@@ -1538,6 +1641,12 @@
1538 logger.debug('%s: started (window size %r).',1641 logger.debug('%s: started (window size %r).',
1539 self.__class__.__name__, self.get_size_request())1642 self.__class__.__name__, self.get_size_request())
15401643
1644 def shutdown(self, *args, **kwargs):
1645 """Shutdown backend."""
1646 logger.info('Shutting down...')
1647 self.backend.shutdown(reply_handler=NO_OP,
1648 error_handler=error_handler)
1649
1541 def on_show_overview_panel(self, widget=None):1650 def on_show_overview_panel(self, widget=None):
1542 """Show the overview panel."""1651 """Show the overview panel."""
1543 self.set_current_page(0)1652 self.set_current_page(0)
@@ -1550,18 +1659,42 @@
1550 if credentials_are_new:1659 if credentials_are_new:
1551 # redirect user to services page to start using Ubuntu One1660 # redirect user to services page to start using Ubuntu One
1552 self.management.services_button.clicked()1661 self.management.services_button.clicked()
1662 # instruct syncdaemon to connect
1663 self.backend.connect_files(reply_handler=NO_OP,
1664 error_handler=error_handler)
15531665
1554 self.next_page()1666 self.next_page()
15551667
15561668
1669class ControlPanelService(dbus.service.Object):
1670 """DBUS service that exposes some of the window's methods."""
1671
1672 def __init__(self, window):
1673 self.window = window
1674 bus_name = dbus.service.BusName(
1675 DBUS_BUS_NAME_GUI, bus=dbus.SessionBus())
1676 dbus.service.Object.__init__(
1677 self, bus_name=bus_name, object_path=DBUS_PATH_GUI)
1678
1679 @log_call(logger.debug)
1680 @dbus.service.method(dbus_interface=DBUS_IFACE_GUI, in_signature='sb')
1681 def switch_to_alert(self, panel='', alert=False):
1682 """Switch to named panel."""
1683 if panel:
1684 self.window.switch_to(panel)
1685 if alert:
1686 self.window.draw_attention()
1687
1688
1557class ControlPanelWindow(gtk.Window):1689class ControlPanelWindow(gtk.Window):
1558 """The main window for the Ubuntu One control panel."""1690 """The main window for the Ubuntu One control panel."""
15591691
1560 TITLE = _('%(app_name)s Control Panel')1692 TITLE = _('%(app_name)s Control Panel')
15611693
1562 def __init__(self, switch_to=None, alert=False):1694 def __init__(self, switch_to='', alert=False):
1563 super(ControlPanelWindow, self).__init__()1695 super(ControlPanelWindow, self).__init__()
15641696
1697 self.connect('focus-in-event', self.remove_urgency)
1565 self.set_title(self.TITLE % {'app_name': U1_APP_NAME})1698 self.set_title(self.TITLE % {'app_name': U1_APP_NAME})
1566 self.set_position(gtk.WIN_POS_CENTER_ALWAYS)1699 self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
1567 self.set_icon_name('ubuntuone')1700 self.set_icon_name('ubuntuone')
@@ -1569,9 +1702,7 @@
15691702
1570 self.connect('delete-event', lambda w, e: gtk.main_quit())1703 self.connect('delete-event', lambda w, e: gtk.main_quit())
1571 if alert:1704 if alert:
1572 # NOTE this should prevent focus stealing but it does not :(1705 self.draw_attention()
1573 self.present_with_time(1)
1574 self.set_urgency_hint(True)
1575 else:1706 else:
1576 self.present()1707 self.present()
15771708
@@ -1580,17 +1711,35 @@
15801711
1581 logger.info('Starting %s pointing at panel: %r.',1712 logger.info('Starting %s pointing at panel: %r.',
1582 self.__class__.__name__, switch_to)1713 self.__class__.__name__, switch_to)
1583 if switch_to is not None:1714 if switch_to:
1584 button = getattr(self.control_panel.management,1715 self.switch_to(switch_to)
1585 '%s_button' % switch_to, None)
1586 if button is not None:
1587 button.clicked()
1588 else:
1589 logger.warning('Could not start at panel: %r.', switch_to)
15901716
1591 logger.debug('%s: started (window size %r).',1717 logger.debug('%s: started (window size %r).',
1592 self.__class__.__name__, self.get_size_request())1718 self.__class__.__name__, self.get_size_request())
15931719
1720 def remove_urgency(self, *args, **kwargs):
1721 """Remove urgency from the launcher entry."""
1722 if not USE_LIBUNITY:
1723 return
1724 entry = Unity.LauncherEntry.get_for_desktop_id(U1_DOTDESKTOP)
1725 if entry.props.urgent:
1726 self.switch_to('volumes')
1727 entry.props.urgent = False
1728
1729 def draw_attention(self):
1730 """Draw attention to the control panel."""
1731 self.present_with_time(1)
1732 self.set_urgency_hint(True)
1733
1734 def switch_to(self, panel):
1735 """Switch to named panel."""
1736 button = getattr(
1737 self.control_panel.management, '%s_button' % panel, None)
1738 if button is not None:
1739 button.clicked()
1740 else:
1741 logger.warning('Could not start at panel: %r.', panel)
1742
1594 def main(self):1743 def main(self):
1595 """Run the main loop of the widget toolkit."""1744 """Run the main loop of the widget toolkit."""
1596 logger.debug('Starting GTK main loop.')1745 logger.debug('Starting GTK main loop.')
15971746
=== modified file 'ubuntuone/controlpanel/gtk/package_manager.py'
--- ubuntuone/controlpanel/gtk/package_manager.py 2011-02-23 13:57:42 +0000
+++ ubuntuone/controlpanel/gtk/package_manager.py 2011-04-08 19:43:13 +0000
@@ -20,13 +20,14 @@
2020
21import apt21import apt
22import aptdaemon.client22import aptdaemon.client
23# pylint: disable=W0404
23import aptdaemon.enums24import aptdaemon.enums
2425
25try:26try:
26 # Unable to import 'defer', pylint: disable=F0401,E061127 # Unable to import 'defer', pylint: disable=F0401,E0611,W0404
27 from aptdaemon.defer import inline_callbacks, return_value28 from aptdaemon.defer import inline_callbacks, return_value
28except ImportError:29except ImportError:
29 # Unable to import 'defer', pylint: disable=F0401,E061130 # Unable to import 'defer', pylint: disable=F0401,E0611,W0404
30 from defer import inline_callbacks, return_value31 from defer import inline_callbacks, return_value
31from aptdaemon.gtkwidgets import AptProgressBar32from aptdaemon.gtkwidgets import AptProgressBar
3233
3334
=== modified file 'ubuntuone/controlpanel/gtk/tests/__init__.py'
--- ubuntuone/controlpanel/gtk/tests/__init__.py 2011-03-10 02:59:44 +0000
+++ ubuntuone/controlpanel/gtk/tests/__init__.py 2011-04-08 19:43:13 +0000
@@ -180,10 +180,19 @@
180 'replications_info', 'change_replication_settings', # replications180 'replications_info', 'change_replication_settings', # replications
181 'file_sync_status', 'enable_files', 'disable_files', # files181 'file_sync_status', 'enable_files', 'disable_files', # files
182 'connect_files', 'disconnect_files',182 'connect_files', 'disconnect_files',
183 'restart_files', 'start_files', 'stop_files',183 'restart_files', 'start_files', 'stop_files', 'shutdown',
184 ]184 ]
185185
186186
187class FakeControlPanelBackend(FakedDBusBackend):
188 """Fake a Control Panel Service, act as a dbus.Interface."""
189
190 bus_name = gui.DBUS_BUS_NAME_GUI
191 object_path = gui.DBUS_PATH_GUI
192 iface = gui.DBUS_IFACE_GUI
193 exposed_methods = ['draw_attention', 'switch_to']
194
195
187class FakedSessionBus(object):196class FakedSessionBus(object):
188 """Fake a session bus."""197 """Fake a session bus."""
189198
@@ -202,6 +211,9 @@
202 *args, **kwargs)211 *args, **kwargs)
203 if dbus_interface == gui.ubuntu_sso.DBUS_CREDENTIALS_IFACE:212 if dbus_interface == gui.ubuntu_sso.DBUS_CREDENTIALS_IFACE:
204 return FakedSSOBackend(obj, dbus_interface, *args, **kwargs)213 return FakedSSOBackend(obj, dbus_interface, *args, **kwargs)
214 if dbus_interface == gui.DBUS_IFACE_GUI:
215 return FakeControlPanelBackend(
216 obj, dbus_interface, *args, **kwargs)
205217
206218
207class FakedPackageManager(object):219class FakedPackageManager(object):
208220
=== modified file 'ubuntuone/controlpanel/gtk/tests/test_gui.py'
--- ubuntuone/controlpanel/gtk/tests/test_gui.py 2011-03-10 02:59:44 +0000
+++ ubuntuone/controlpanel/gtk/tests/test_gui.py 2011-04-08 19:43:13 +0000
@@ -278,6 +278,7 @@
278278
279 def test_clicking_on_row_opens_folder(self):279 def test_clicking_on_row_opens_folder(self):
280 """The folder activated is opened."""280 """The folder activated is opened."""
281 self.patch(gui.os.path, 'exists', lambda *a: True)
281 self.patch(gui, 'uri_hook', self._set_called)282 self.patch(gui, 'uri_hook', self._set_called)
282 self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)283 self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
283284
@@ -287,6 +288,33 @@
287 self.assertEqual(self._called,288 self.assertEqual(self._called,
288 ((None, gui.FILE_URI_PREFIX + ROOT['path']), {}))289 ((None, gui.FILE_URI_PREFIX + ROOT['path']), {}))
289290
291 def test_clicking_on_row_handles_path_none(self):
292 """None paths are properly handled."""
293 self.patch(gui, 'uri_hook', self._set_called)
294 self.patch(self.ui.volumes_store, 'get_value', lambda *a: None)
295 self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
296
297 self.ui.volumes_view.row_activated('0:0',
298 self.ui.volumes_view.get_column(0))
299
300 self.assertTrue(self.memento.check_warning('tree_path (0, 0)',
301 'volume_path', 'is None'))
302 self.assertEqual(self._called, False)
303
304 def test_clicking_on_row_handles_path_non_existent(self):
305 """Not-existent paths are properly handled."""
306 self.patch(gui.os.path, 'exists', lambda *a: False)
307 self.patch(gui, 'uri_hook', self._set_called)
308 path = 'not-in-disk'
309 self.patch(self.ui.volumes_store, 'get_value', lambda *a: path)
310 self.ui.on_volumes_info_ready(FAKE_VOLUMES_INFO)
311
312 self.ui.volumes_view.row_activated('0:0',
313 self.ui.volumes_view.get_column(0))
314
315 self.assertTrue(self.memento.check_warning(path, 'does not exist'))
316 self.assertEqual(self._called, False)
317
290 def test_on_volumes_info_ready_with_music_folder(self):318 def test_on_volumes_info_ready_with_music_folder(self):
291 """The volumes info is processed when ready."""319 """The volumes info is processed when ready."""
292 info = [(u'', u'147852369', [ROOT] + [MUSIC_FOLDER])]320 info = [(u'', u'147852369', [ROOT] + [MUSIC_FOLDER])]
@@ -1646,12 +1674,15 @@
1646 ('FileSyncStatusIdle', [self.ui.on_file_sync_status_idle]),1674 ('FileSyncStatusIdle', [self.ui.on_file_sync_status_idle]),
1647 ('FileSyncStatusError', [self.ui.on_file_sync_status_error]),1675 ('FileSyncStatusError', [self.ui.on_file_sync_status_error]),
1648 ('FilesStartError', [self.ui.on_files_start_error]),1676 ('FilesStartError', [self.ui.on_files_start_error]),
1677 ('FilesDisabled', [self.ui.on_file_sync_status_disabled]),
1678 ('FilesEnabled', [self.ui.on_file_sync_status_starting]),
1649 )1679 )
1650 for sig, handlers in matches:1680 for sig, handlers in matches:
1651 self.assertEqual(self.ui.backend._signals[sig], handlers)1681 self.assertEqual(self.ui.backend._signals[sig], handlers)
16521682
1653 def test_file_sync_status_is_requested(self):1683 def test_file_sync_status_is_requested_on_load(self):
1654 """The file sync status is requested to the backend."""1684 """The file sync status is requested to the backend."""
1685 self.ui.load()
1655 self.assert_backend_called('file_sync_status', ())1686 self.assert_backend_called('file_sync_status', ())
16561687
1657 def test_on_file_sync_status_disabled(self):1688 def test_on_file_sync_status_disabled(self):
@@ -1882,11 +1913,32 @@
1882 self.assertEqual(self.ui.backend._signals['AccountInfoError'],1913 self.assertEqual(self.ui.backend._signals['AccountInfoError'],
1883 [self.ui.on_account_info_error])1914 [self.ui.on_account_info_error])
18841915
1916 def test_backend_unauthorized_signal(self):
1917 """The proper signals are connected to the backend."""
1918 self.assertEqual(self.ui.backend._signals['UnauthorizedError'],
1919 [self.ui.on_unauthorized_error])
1920
1921 def test_no_backend_calls_before_load(self):
1922 """No calls are made to the backend before load() is called."""
1923 self.assertEqual(self.ui.backend._called, {})
1924
1885 def test_account_info_is_requested_on_load(self):1925 def test_account_info_is_requested_on_load(self):
1886 """The account info is requested to the backend."""1926 """The account info is requested to the backend."""
1887 self.ui.load()1927 self.ui.load()
1888 self.assert_backend_called('account_info', ())1928 self.assert_backend_called('account_info', ())
18891929
1930 def test_file_sync_status_info_is_requested_on_load(self):
1931 """The file sync status info is requested to the backend."""
1932 self.patch(self.ui.status_label, 'load', self._set_called)
1933 self.ui.load()
1934 self.assertEqual(self._called, ((), {}))
1935
1936 def test_replications_info_is_requested_on_load(self):
1937 """The replications info is requested to the backend."""
1938 self.patch(self.ui.services, 'load', self._set_called)
1939 self.ui.load()
1940 self.assertEqual(self._called, ((), {}))
1941
1890 def test_dashboard_panel_is_packed(self):1942 def test_dashboard_panel_is_packed(self):
1891 """The dashboard panel is packed."""1943 """The dashboard panel is packed."""
1892 self.assertIsInstance(self.ui.dashboard, gui.DashboardPanel)1944 self.assertIsInstance(self.ui.dashboard, gui.DashboardPanel)
@@ -1999,6 +2051,23 @@
1999 self.assertIsInstance(self.ui.status_label, gui.FileSyncStatus)2051 self.assertIsInstance(self.ui.status_label, gui.FileSyncStatus)
2000 self.assertIn(self.ui.status_label, self.ui.status_box.get_children())2052 self.assertIn(self.ui.status_label, self.ui.status_box.get_children())
20012053
2054 def test_backend_file_sync_signals(self):
2055 """The proper signals are connected to the backend."""
2056 self.assertEqual(self.ui.backend._signals['FilesEnabled'],
2057 [self.ui.enable_volumes])
2058 self.assertEqual(self.ui.backend._signals['FilesDisabled'],
2059 [self.ui.disable_volumes])
2060
2061 def test_enable_volumes(self):
2062 """The volumes tab is properly enabled."""
2063 self.ui.enable_volumes()
2064 self.assertTrue(self.ui.volumes_button.get_sensitive())
2065
2066 def test_disable_volumes(self):
2067 """The volumes tab is properly disabled."""
2068 self.ui.disable_volumes()
2069 self.assertFalse(self.ui.volumes_button.get_sensitive())
2070
2002 def test_local_device_removed_is_emitted(self):2071 def test_local_device_removed_is_emitted(self):
2003 """Signal local-device-removed is sent when DevicesPanel emits it."""2072 """Signal local-device-removed is sent when DevicesPanel emits it."""
2004 self.ui.connect('local-device-removed', self._set_called)2073 self.ui.connect('local-device-removed', self._set_called)
@@ -2024,3 +2093,9 @@
2024 actual = getattr(self.ui, '%s_button' % tab).get_tooltip_text()2093 actual = getattr(self.ui, '%s_button' % tab).get_tooltip_text()
2025 expected = getattr(self.ui, '%s_BUTTON_TOOLTIP' % tab.upper())2094 expected = getattr(self.ui, '%s_BUTTON_TOOLTIP' % tab.upper())
2026 self.assertEqual(actual, expected)2095 self.assertEqual(actual, expected)
2096
2097 def test_on_unauthorized_error(self):
2098 """On invalid credentials, proper signal is sent."""
2099 self.ui.connect('unauthorized', self._set_called)
2100 self.ui.on_unauthorized_error()
2101 self.assertEqual(self._called, ((self.ui,), {}))
20272102
=== modified file 'ubuntuone/controlpanel/gtk/tests/test_gui_basic.py'
--- ubuntuone/controlpanel/gtk/tests/test_gui_basic.py 2011-03-23 16:06:41 +0000
+++ ubuntuone/controlpanel/gtk/tests/test_gui_basic.py 2011-04-08 19:43:13 +0000
@@ -29,6 +29,43 @@
29# pylint: disable=W0201, W021229# pylint: disable=W0201, W0212
3030
3131
32class FakeLauncherEntryProps(object):
33 """A fake Unity.LauncherEntry.props"""
34
35 urgent = True
36
37
38THE_FLEP = FakeLauncherEntryProps()
39
40
41class FakeLauncherEntry(object):
42 """A fake Unity.LauncherEntry"""
43
44 def __init__(self):
45 """Initialize this fake instance."""
46 self.props = THE_FLEP
47
48 @staticmethod
49 def get_for_desktop_id(dotdesktop):
50 """Find the LauncherEntry for a given dotdesktop."""
51 return FakeLauncherEntry()
52
53
54class FakeControlPanelService(object):
55 """Fake service."""
56 def __init__(self, window):
57 self.window = window
58 self.called = []
59
60 def draw_attention(self):
61 """Draw attention to the control panel."""
62 self.called.append('draw_attention')
63
64 def switch_to(self, panel):
65 """Switch to named panel."""
66 self.called.append(('switch_to', panel))
67
68
32class ControlPanelMixinTestCase(BaseTestCase):69class ControlPanelMixinTestCase(BaseTestCase):
33 """The test suite for the control panel widget."""70 """The test suite for the control panel widget."""
3471
@@ -49,25 +86,30 @@
4986
50 klass = gui.ControlPanelWindow87 klass = gui.ControlPanelWindow
5188
89 def setUp(self):
90 self.patch(gui, 'ControlPanelService', FakeControlPanelService)
91 super(ControlPanelWindowTestCase, self).setUp()
92
52 def test_is_a_window(self):93 def test_is_a_window(self):
53 """Inherits from gtk.Window."""94 """Inherits from gtk.Window."""
54 self.assertIsInstance(self.ui, gui.gtk.Window)95 self.assertIsInstance(self.ui, gui.gtk.Window)
5596
56 def test_startup_visibility(self):97 def test_startup_visibility(self):
57 """The widget is visible at startup."""98 """The widget is visible at startup."""
58 self.assertTrue(self.ui.get_visible(), 'must be visible at startup.')99 self.assertTrue(self.ui.get_visible(), 'was not visible at startup.')
59100
60 def test_main_start_gtk_main_loop(self):101 def test_main_start_gtk_main_loop(self):
61 """The GTK main loop is started when calling main()."""102 """The GTK main loop is started when calling main()."""
62 self.patch(gui.gtk, 'main', self._set_called)103 self.patch(gui.gtk, 'main', self._set_called)
63 self.ui.main()104 self.ui.main()
64 self.assertEqual(self._called, ((), {}), 'gtk.main was called.')105 self.assertEqual(self._called, ((), {}), 'gtk.main was not called.')
65106
66 def test_closing_stops_the_main_lopp(self):107 def test_closing_stops_the_main_lopp(self):
67 """The GTK main loop is stopped when closing the window."""108 """The GTK main loop is stopped when closing the window."""
68 self.patch(gui.gtk, 'main_quit', self._set_called)109 self.patch(gui.gtk, 'main_quit', self._set_called)
69 self.ui.emit('delete-event', None)110 self.ui.emit('delete-event', None)
70 self.assertEqual(self._called, ((), {}), 'gtk.main_quit was called.')111 self.assertEqual(
112 self._called, ((), {}), 'gtk.main_quit was not called.')
71113
72 def test_title_is_correct(self):114 def test_title_is_correct(self):
73 """The window title is correct."""115 """The window title is correct."""
@@ -96,6 +138,32 @@
96 """Max size is not bigger than 736x525 (LP: #645526, LP: #683164)."""138 """Max size is not bigger than 736x525 (LP: #645526, LP: #683164)."""
97 self.assertTrue(self.ui.get_size_request() <= (736, 525))139 self.assertTrue(self.ui.get_size_request() <= (736, 525))
98140
141 def test_focus_handler(self):
142 """When the window receives focus, the handler is called."""
143 THE_FLEP.urgent = True
144 self.patch(gui.Unity, "LauncherEntry", FakeLauncherEntry)
145 cp = gui.ControlPanelWindow()
146 cp.emit('focus-in-event', None)
147 self.assertEqual(
148 False, THE_FLEP.urgent, 'remove_urgency should have been called.')
149
150
151class ControlPanelWindowAlertParamTestCase(BaseTestCase):
152 """The test suite for the control panel window when passing params."""
153
154 klass = gui.ControlPanelWindow
155 kwargs = {'alert': True}
156
157 def setUp(self):
158 self.patch(gui, 'ControlPanelService', FakeControlPanelService)
159 self.patch(gui.ControlPanelWindow, 'draw_attention', self._set_called)
160 super(ControlPanelWindowAlertParamTestCase, self).setUp()
161
162 def test_alert(self):
163 """Can pass a 'alert' parameter to draw attention to the window."""
164 self.assertEqual(
165 ((), {}), self._called, 'draw_attention should have been called.')
166
99167
100class ControlPanelWindowParamsTestCase(ControlPanelWindowTestCase):168class ControlPanelWindowParamsTestCase(ControlPanelWindowTestCase):
101 """The test suite for the control panel window when passing params."""169 """The test suite for the control panel window when passing params."""
@@ -169,8 +237,10 @@
169237
170 def test_on_show_management_panel(self):238 def test_on_show_management_panel(self):
171 """A ManagementPanel is shown when the callback is executed."""239 """A ManagementPanel is shown when the callback is executed."""
240 self.patch(self.ui.management, 'load', self._set_called)
172 self.ui.on_show_management_panel()241 self.ui.on_show_management_panel()
173 self.assert_current_tab_correct(self.ui.management)242 self.assert_current_tab_correct(self.ui.management)
243 self.assertEqual(self._called, ((), {}))
174244
175 def test_on_show_management_panel_is_idempotent(self):245 def test_on_show_management_panel_is_idempotent(self):
176 """Only one ManagementPanel is shown."""246 """Only one ManagementPanel is shown."""
@@ -179,7 +249,7 @@
179249
180 self.assert_current_tab_correct(self.ui.management)250 self.assert_current_tab_correct(self.ui.management)
181251
182 def test_credentials_found_shows_dashboard_management_panel(self):252 def test_credentials_found_shows_dashboard_panel(self):
183 """On 'credentials-found' signal, the management panel is shown.253 """On 'credentials-found' signal, the management panel is shown.
184254
185 If first signal parameter is False, visible tab should be dashboard.255 If first signal parameter is False, visible tab should be dashboard.
@@ -193,7 +263,7 @@
193 self.ui.management.DASHBOARD_PAGE)263 self.ui.management.DASHBOARD_PAGE)
194 self.assertEqual(self._called, ((), {}))264 self.assertEqual(self._called, ((), {}))
195265
196 def test_credentials_found_shows_volumes_management_panel(self):266 def test_credentials_found_shows_services_panel(self):
197 """On 'credentials-found' signal, the management panel is shown.267 """On 'credentials-found' signal, the management panel is shown.
198268
199 If first signal parameter is True, visible tab should be services.269 If first signal parameter is True, visible tab should be services.
@@ -206,6 +276,12 @@
206 self.assertEqual(self.ui.management.notebook.get_current_page(),276 self.assertEqual(self.ui.management.notebook.get_current_page(),
207 self.ui.management.SERVICES_PAGE)277 self.ui.management.SERVICES_PAGE)
208278
279 def test_credentials_found_connects_syncdaemon(self):
280 """On 'credentials-found' signal, ask syncdaemon to connect."""
281 # credentials are new
282 self.ui.overview.emit('credentials-found', True, object())
283 self.assert_backend_called('connect_files', ())
284
209 def test_local_device_removed_shows_overview_panel(self):285 def test_local_device_removed_shows_overview_panel(self):
210 """On 'local-device-removed' signal, the overview panel is shown."""286 """On 'local-device-removed' signal, the overview panel is shown."""
211 self.ui.overview.emit('credentials-found', True, object())287 self.ui.overview.emit('credentials-found', True, object())
@@ -213,6 +289,18 @@
213289
214 self.assert_current_tab_correct(self.ui.overview)290 self.assert_current_tab_correct(self.ui.overview)
215291
292 def test_unauthorized_shows_overview_panel(self):
293 """On 'unauthorized' signal, the overview panel is shown."""
294 self.ui.overview.emit('credentials-found', True, object())
295 self.ui.management.emit('unauthorized')
296
297 self.assert_current_tab_correct(self.ui.overview)
298
299 def test_backend_is_shutdown_on_close(self):
300 """When the control panel is closed, the backend is shutdown."""
301 self.ui.emit('destroy')
302 self.assert_backend_called('shutdown', ())
303
216304
217class UbuntuOneBinTestCase(BaseTestCase):305class UbuntuOneBinTestCase(BaseTestCase):
218 """The test suite for a Ubuntu One panel."""306 """The test suite for a Ubuntu One panel."""
@@ -276,6 +364,42 @@
276 self.assert_warning_correct(self.ui.message, msg)364 self.assert_warning_correct(self.ui.message, msg)
277 self.assertFalse(self.ui.message.active)365 self.assertFalse(self.ui.message.active)
278366
367 def test_on_error_with_error_dict(self):
368 """Callback to stop the Loading and show the error from error_dict."""
369 msg = u'Qué mala suerte! <i>this does not rock</i>'
370 error_dict = {gui.ERROR_TYPE: 'YaddaError', gui.ERROR_MESSAGE: msg}
371 self.ui.on_error(error_dict=error_dict)
372
373 expected_msg = "%s (%s: %s)" % (gui.VALUE_ERROR, 'YaddaError', msg)
374 self.assert_warning_correct(self.ui.message, expected_msg)
375 self.assertFalse(self.ui.message.active)
376
377 def test_on_error_with_error_dict_without_error_type(self):
378 """Callback to stop the Loading and show the error from error_dict."""
379 error_dict = {}
380 self.ui.on_error(error_dict=error_dict)
381
382 expected_msg = "%s (%s)" % (gui.VALUE_ERROR, gui.UNKNOWN_ERROR)
383 self.assert_warning_correct(self.ui.message, expected_msg)
384 self.assertFalse(self.ui.message.active)
385
386 def test_on_error_with_error_dict_without_error_message(self):
387 """Callback to stop the Loading and show the error from error_dict."""
388 error_dict = {gui.ERROR_TYPE: 'YaddaError'}
389 self.ui.on_error(error_dict=error_dict)
390
391 expected_msg = "%s (%s)" % (gui.VALUE_ERROR, 'YaddaError')
392 self.assert_warning_correct(self.ui.message, expected_msg)
393 self.assertFalse(self.ui.message.active)
394
395 def test_on_error_with_message_and_error_dict(self):
396 """Callback to stop the Loading and show a info message."""
397 error_dict = {gui.ERROR_TYPE: 'YaddaError', gui.ERROR_MESSAGE: 'test'}
398 msg = 'WOW! <i>this does not rock</i> :-/'
399 self.ui.on_error(message=msg, error_dict=error_dict)
400 self.assert_warning_correct(self.ui.message, msg)
401 self.assertFalse(self.ui.message.active)
402
279 def test_is_processing(self):403 def test_is_processing(self):
280 """The flag 'is_processing' is False on start."""404 """The flag 'is_processing' is False on start."""
281 self.assertFalse(self.ui.is_processing)405 self.assertFalse(self.ui.is_processing)
282406
=== modified file 'ubuntuone/controlpanel/gtk/tests/test_package_manager.py'
--- ubuntuone/controlpanel/gtk/tests/test_package_manager.py 2011-01-07 20:07:39 +0000
+++ ubuntuone/controlpanel/gtk/tests/test_package_manager.py 2011-04-08 19:43:13 +0000
@@ -21,10 +21,10 @@
21import collections21import collections
2222
23try:23try:
24 # Unable to import 'defer', pylint: disable=F0401,E061124 # Unable to import 'defer', pylint: disable=F0401,E0611,W0404
25 from aptdaemon import defer25 from aptdaemon import defer
26except ImportError:26except ImportError:
27 # Unable to import 'defer', pylint: disable=F0401,E061127 # Unable to import 'defer', pylint: disable=F0401,E0611,W0404
28 import defer28 import defer
2929
30from ubuntuone.controlpanel.gtk import package_manager30from ubuntuone.controlpanel.gtk import package_manager
3131
=== modified file 'ubuntuone/controlpanel/integrationtests/__init__.py'
--- ubuntuone/controlpanel/integrationtests/__init__.py 2010-12-22 13:33:25 +0000
+++ ubuntuone/controlpanel/integrationtests/__init__.py 2011-04-08 19:43:13 +0000
@@ -20,7 +20,6 @@
20"""Integration tests for the Ubuntu One Control Panel."""20"""Integration tests for the Ubuntu One Control Panel."""
2121
22import dbus22import dbus
23import dbus.service
2423
25from ubuntuone.devtools.testcase import DBusTestCase as TestCase24from ubuntuone.devtools.testcase import DBusTestCase as TestCase
2625
@@ -31,7 +30,7 @@
31 """A DBus exception to be used in tests."""30 """A DBus exception to be used in tests."""
3231
3332
34class MockDBusNoMethods(dbus.service.Object):33class MockDBusNoMethods(dbus_service.dbus.service.Object):
35 """A mock that fails at the DBus layer (because it's got no methods!)."""34 """A mock that fails at the DBus layer (because it's got no methods!)."""
3635
3736
3837
=== modified file 'ubuntuone/controlpanel/integrationtests/test_dbus_service.py'
--- ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2011-02-23 13:57:42 +0000
+++ ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2011-04-08 19:43:13 +0000
@@ -84,12 +84,23 @@
84 rs = self.mocker.replace(rs_name)84 rs = self.mocker.replace(rs_name)
85 rs()85 rs()
86 self.mocker.result(True)86 self.mocker.result(True)
87
88 mainloop = "ubuntuone.controlpanel.dbus_service.gobject.MainLoop"
89 mainloop = self.mocker.replace(mainloop)
90 mainloop()
91 loop = self.mocker.mock()
92 self.mocker.result(loop)
93
94 shutdown_func = self.mocker.mock()
95 loop.quit # pylint: disable=W0104
96 self.mocker.result(shutdown_func)
97
87 rml_name = "ubuntuone.controlpanel.dbus_service.run_mainloop"98 rml_name = "ubuntuone.controlpanel.dbus_service.run_mainloop"
88 rml = self.mocker.replace(rml_name)99 rml = self.mocker.replace(rml_name)
89 rml()100 rml(loop=loop)
90 pb_name = "ubuntuone.controlpanel.dbus_service.publish_backend"101 pb_name = "ubuntuone.controlpanel.dbus_service.publish_backend"
91 pb = self.mocker.replace(pb_name)102 pb = self.mocker.replace(pb_name)
92 pb()103 pb(shutdown_func=shutdown_func)
93 self.mocker.replay()104 self.mocker.replay()
94 dbus_service.main()105 dbus_service.main()
95106
@@ -112,6 +123,10 @@
112 dbus_service.STATUS_KEY: dbus_service.FILE_SYNC_IDLE,123 dbus_service.STATUS_KEY: dbus_service.FILE_SYNC_IDLE,
113 }124 }
114 status_changed_handler = None125 status_changed_handler = None
126 shutdown_func = None
127
128 def __init__(self, shutdown_func=None):
129 MockBackend.shutdown_func = shutdown_func
115130
116 def _process(self, result):131 def _process(self, result):
117 """Process the request with the given result."""132 """Process the request with the given result."""
@@ -197,6 +212,10 @@
197 """Install the extension to sync bookmarks."""212 """Install the extension to sync bookmarks."""
198 return self._process(None)213 return self._process(None)
199214
215 def shutdown(self):
216 """Stop this service."""
217 self.shutdown_func()
218
200219
201class DBusServiceTestCase(TestCase):220class DBusServiceTestCase(TestCase):
202 """Test for the DBus service."""221 """Test for the DBus service."""
@@ -289,7 +308,8 @@
289 def setUp(self):308 def setUp(self):
290 super(BaseTestCase, self).setUp()309 super(BaseTestCase, self).setUp()
291 dbus_service.init_mainloop()310 dbus_service.init_mainloop()
292 be = dbus_service.publish_backend(MockBackend())311 self.patch(dbus_service, 'ControlBackend', MockBackend)
312 be = dbus_service.publish_backend()
293 self.addCleanup(be.remove_from_connection)313 self.addCleanup(be.remove_from_connection)
294 bus = dbus.SessionBus()314 bus = dbus.SessionBus()
295 obj = bus.get_object(bus_name=DBUS_BUS_NAME,315 obj = bus.get_object(bus_name=DBUS_BUS_NAME,
@@ -604,6 +624,35 @@
604 error_sig, success_sig, got_error_signal, method, *args)624 error_sig, success_sig, got_error_signal, method, *args)
605625
606626
627class OperationsAuthErrorTestCase(OperationsTestCase):
628 """Test for the DBus service operations when UnauthorizedError happens."""
629
630 def setUp(self):
631 super(OperationsAuthErrorTestCase, self).setUp()
632 self.patch(MockBackend, 'exception',
633 dbus_service.UnauthorizedError)
634
635 def assert_correct_method_call(self, success_sig, error_sig, success_cb,
636 method, *args):
637 """Call parent instance expecting UnauthorizedError signal."""
638
639 def inner_success_cb(*a):
640 """The success signal was received."""
641 if len(a) == 1:
642 error_dict = a[0]
643 else:
644 an_id, error_dict = a
645 self.assertEqual(an_id, args[0])
646
647 self.assertEqual(error_dict[dbus_service.ERROR_TYPE],
648 'UnauthorizedError')
649 self.deferred.callback('success')
650
651 parent = super(OperationsAuthErrorTestCase, self)
652 return parent.assert_correct_method_call(
653 "UnauthorizedError", error_sig, inner_success_cb, method, *args)
654
655
607class FileSyncTestCase(BaseTestCase):656class FileSyncTestCase(BaseTestCase):
608 """Test for the DBus service when requesting file sync status."""657 """Test for the DBus service when requesting file sync status."""
609658
@@ -691,3 +740,18 @@
691 cpbe = dbus_service.ControlPanelBackend(backend=be)740 cpbe = dbus_service.ControlPanelBackend(backend=be)
692741
693 self.assertEqual(be.status_changed_handler, cpbe.process_status)742 self.assertEqual(be.status_changed_handler, cpbe.process_status)
743
744
745class ShutdownTestCase(BaseTestCase):
746 """Test for the DBus service shurdown."""
747
748 @defer.inlineCallbacks
749 def test_shutdown(self):
750 """The service can be shutdown."""
751 called = []
752 MockBackend.shutdown_func = lambda *a: called.append('shutdown')
753 self.backend.shutdown(reply_handler=lambda: self.deferred.callback(1),
754 error_handler=self.got_error)
755 yield self.deferred
756
757 self.assertEqual(called, ['shutdown'])
694758
=== added file 'ubuntuone/controlpanel/integrationtests/test_gui_service.py'
--- ubuntuone/controlpanel/integrationtests/test_gui_service.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/controlpanel/integrationtests/test_gui_service.py 2011-04-08 19:43:13 +0000
@@ -0,0 +1,98 @@
1# -*- coding: utf-8 -*-
2
3# Authors: Alejandro J. Cura <alecu@canonical.com>
4# Natalia B. Bidart <nataliabidart@canonical.com>
5# Eric Casteleijn <eric.casteleijn@canonical.com>
6#
7# Copyright 2011 Canonical Ltd.
8#
9# This program is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License version 3, as published
11# by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranties of
15# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16# PURPOSE. See the GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program. If not, see <http://www.gnu.org/licenses/>.
20
21"""Tests for the control panel backend DBus service."""
22
23import dbus
24import mocker
25
26from dbus.mainloop.glib import DBusGMainLoop
27
28from ubuntuone.controlpanel.gtk import gui
29from ubuntuone.devtools.testcase import DBusTestCase
30from twisted.trial.unittest import TestCase
31
32
33class MockWindow(object):
34 """A mock backend."""
35
36 exception = None
37
38 def __init__(self, switch_to=None, alert=False):
39 self.called = []
40
41 def draw_attention(self):
42 """Draw attention to the control panel."""
43 self.called.append('draw_attention')
44
45 def switch_to(self, panel):
46 """Switch to named panel."""
47 self.called.append(('switch_to', panel))
48
49
50class DBusServiceMockTestCase(TestCase):
51 """Tests for the main function."""
52
53 def setUp(self):
54 self.mocker = mocker.Mocker()
55
56 def tearDown(self):
57 self.mocker.restore()
58 self.mocker.verify()
59
60 def test_dbus_service_main(self):
61 """The main method starts the loop and hooks up to DBus."""
62 self.patch(gui, 'ControlPanelWindow', MockWindow)
63 dbus_gmain_loop = self.mocker.replace(
64 "dbus.mainloop.glib.DBusGMainLoop")
65 register_service = self.mocker.replace(
66 "ubuntuone.controlpanel.gtk.gui.register_service")
67 publish_service = self.mocker.replace(
68 "ubuntuone.controlpanel.gtk.gui.publish_service")
69 main = self.mocker.replace("gtk.main")
70 dbus_gmain_loop(set_as_default=True)
71 loop = self.mocker.mock()
72 self.mocker.result(loop)
73 register_service(mocker.ANY)
74 self.mocker.result(True)
75 publish_service(switch_to='', alert=False)
76 main()
77 self.mocker.replay()
78 gui.main()
79
80
81class DBusServiceTestCase(DBusTestCase):
82 """Test for the DBus service."""
83
84 def _set_called(self, *args, **kwargs):
85 """Keep track of function calls, useful for monkeypatching."""
86 self._called = (args, kwargs)
87
88 def setUp(self):
89 """Initialize each test run."""
90 super(DBusServiceTestCase, self).setUp()
91 DBusGMainLoop(set_as_default=True)
92 self._called = False
93
94 def test_register_service(self):
95 """The DBus service is successfully registered."""
96 bus = dbus.SessionBus()
97 ret = gui.register_service(bus)
98 self.assertTrue(ret)
099
=== modified file 'ubuntuone/controlpanel/integrationtests/test_webclient.py'
--- ubuntuone/controlpanel/integrationtests/test_webclient.py 2010-12-06 12:27:11 +0000
+++ ubuntuone/controlpanel/integrationtests/test_webclient.py 2011-04-08 19:43:13 +0000
@@ -73,6 +73,10 @@
73 devices_resource.contents = SAMPLE_RESOURCE73 devices_resource.contents = SAMPLE_RESOURCE
74 root.putChild("devices", devices_resource)74 root.putChild("devices", devices_resource)
75 root.putChild("throwerror", resource.NoResource())75 root.putChild("throwerror", resource.NoResource())
76 unauthorized = resource.ErrorPage(resource.http.UNAUTHORIZED,
77 "Unauthrorized", "Unauthrorized")
78 root.putChild("unauthorized", unauthorized)
79
76 site = server.Site(root)80 site = server.Site(root)
77 application = service.Application('web')81 application = service.Application('web')
78 self.service_collection = service.IServiceCollection(application)82 self.service_collection = service.IServiceCollection(application)
@@ -96,7 +100,7 @@
96class WebClientTestCase(TestCase):100class WebClientTestCase(TestCase):
97 """Test for the webservice client."""101 """Test for the webservice client."""
98102
99 timeout = 5103 timeout = 8
100104
101 def setUp(self):105 def setUp(self):
102 super(WebClientTestCase, self).setUp()106 super(WebClientTestCase, self).setUp()
@@ -123,6 +127,12 @@
123 yield self.assertFailure(self.wc.call_api("throwerror"),127 yield self.assertFailure(self.wc.call_api("throwerror"),
124 webclient.WebClientError)128 webclient.WebClientError)
125129
130 @inlineCallbacks
131 def test_unauthorized(self):
132 """Detect when a request failed with UNAUTHORIZED."""
133 yield self.assertFailure(self.wc.call_api("unauthorized"),
134 webclient.UnauthorizedError)
135
126136
127class OAuthTestCase(TestCase):137class OAuthTestCase(TestCase):
128 """Test for the oauth signing code."""138 """Test for the oauth signing code."""
129139
=== modified file 'ubuntuone/controlpanel/replication_client.py'
--- ubuntuone/controlpanel/replication_client.py 2011-01-07 20:07:39 +0000
+++ ubuntuone/controlpanel/replication_client.py 2011-04-08 19:43:13 +0000
@@ -57,7 +57,7 @@
57 if replication_module is None:57 if replication_module is None:
58 # delay import in case DC is not installed at module import time58 # delay import in case DC is not installed at module import time
59 # Unable to import 'desktopcouch.application.replication_services'59 # Unable to import 'desktopcouch.application.replication_services'
60 # pylint: disable=F040160 # pylint: disable=W0404
61 from desktopcouch.application.replication_services \61 from desktopcouch.application.replication_services \
62 import ubuntuone as replication_module62 import ubuntuone as replication_module
63 try:63 try:
6464
=== modified file 'ubuntuone/controlpanel/tests/__init__.py'
--- ubuntuone/controlpanel/tests/__init__.py 2011-03-10 02:59:44 +0000
+++ ubuntuone/controlpanel/tests/__init__.py 2011-04-08 19:43:13 +0000
@@ -23,7 +23,7 @@
2323
24TOKEN = {u'consumer_key': u'xQ7xDAz',24TOKEN = {u'consumer_key': u'xQ7xDAz',
25 u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy',25 u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy',
26 u'token_name': u'test',26 u'name': u'Ubuntu One @ localhost',
27 u'token': u'ABCDEF01234-localtoken',27 u'token': u'ABCDEF01234-localtoken',
28 u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}28 u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
2929
@@ -110,19 +110,31 @@
110]110]
111"""111"""
112112
113EMPTY_DESCRIPTION_JSON = """
114[
115 {
116 "token": "ABCDEF01234token",
117 "description": null,
118 "kind": "Computer"
119 }
120]
121"""
122
123LOCAL_DEVICE = {
124 'is_local': 'True',
125 'configurable': 'True',
126 'device_id': 'ComputerABCDEF01234-localtoken',
127 'limit_bandwidth': '',
128 'show_all_notifications': 'True',
129 'max_download_speed': '-1',
130 'max_upload_speed': '-1',
131 'name': 'Ubuntu One @ localhost',
132 'type': 'Computer',
133}
134
113# note that local computer should be first, do not change!135# note that local computer should be first, do not change!
114EXPECTED_DEVICES_INFO = [136EXPECTED_DEVICES_INFO = [
115 {137 LOCAL_DEVICE,
116 'is_local': 'True',
117 'configurable': 'True',
118 'device_id': 'ComputerABCDEF01234-localtoken',
119 'limit_bandwidth': '',
120 'show_all_notifications': 'True',
121 'max_download_speed': '-1',
122 'max_upload_speed': '-1',
123 'name': 'Ubuntu One @ localhost',
124 'type': 'Computer',
125 },
126 {138 {
127 "device_id": "ComputerABCDEF01234token",139 "device_id": "ComputerABCDEF01234token",
128 "name": "Ubuntu One @ darkstar",140 "name": "Ubuntu One @ darkstar",
129141
=== modified file 'ubuntuone/controlpanel/tests/test_backend.py'
--- ubuntuone/controlpanel/tests/test_backend.py 2011-03-10 02:59:44 +0000
+++ ubuntuone/controlpanel/tests/test_backend.py 2011-04-08 19:43:13 +0000
@@ -30,6 +30,7 @@
30from ubuntuone.controlpanel import backend, replication_client30from ubuntuone.controlpanel import backend, replication_client
31from ubuntuone.controlpanel.backend import (bool_str,31from ubuntuone.controlpanel.backend import (bool_str,
32 ACCOUNT_API, DEVICES_API, DEVICE_REMOVE_API, QUOTA_API,32 ACCOUNT_API, DEVICES_API, DEVICE_REMOVE_API, QUOTA_API,
33 DEVICE_TYPE_COMPUTER,
33 FILE_SYNC_DISABLED,34 FILE_SYNC_DISABLED,
34 FILE_SYNC_DISCONNECTED,35 FILE_SYNC_DISCONNECTED,
35 FILE_SYNC_ERROR,36 FILE_SYNC_ERROR,
@@ -41,9 +42,11 @@
41 MSG_KEY, STATUS_KEY,42 MSG_KEY, STATUS_KEY,
42)43)
43from ubuntuone.controlpanel.tests import (TestCase,44from ubuntuone.controlpanel.tests import (TestCase,
45 EMPTY_DESCRIPTION_JSON,
44 EXPECTED_ACCOUNT_INFO,46 EXPECTED_ACCOUNT_INFO,
45 EXPECTED_ACCOUNT_INFO_WITH_CURRENT_PLAN,47 EXPECTED_ACCOUNT_INFO_WITH_CURRENT_PLAN,
46 EXPECTED_DEVICES_INFO,48 EXPECTED_DEVICES_INFO,
49 LOCAL_DEVICE,
47 ROOT_PATH,50 ROOT_PATH,
48 SAMPLE_ACCOUNT_NO_CURRENT_PLAN,51 SAMPLE_ACCOUNT_NO_CURRENT_PLAN,
49 SAMPLE_ACCOUNT_WITH_CURRENT_PLAN,52 SAMPLE_ACCOUNT_WITH_CURRENT_PLAN,
@@ -56,7 +59,6 @@
56 SHARES_PATH_LINK,59 SHARES_PATH_LINK,
57 TOKEN,60 TOKEN,
58)61)
59from ubuntuone.controlpanel.webclient import WebClientError
6062
6163
62class MockWebClient(object):64class MockWebClient(object):
@@ -70,8 +72,10 @@
7072
71 def call_api(self, method):73 def call_api(self, method):
72 """Get a given url from the webservice."""74 """Get a given url from the webservice."""
73 if self.failure:75 if self.failure == 401:
74 return defer.fail(WebClientError(self.failure))76 return defer.fail(backend.UnauthorizedError(self.failure))
77 elif self.failure:
78 return defer.fail(backend.WebClientError(self.failure))
75 else:79 else:
76 result = simplejson.loads(self.results[method])80 result = simplejson.loads(self.results[method])
77 return defer.succeed(result)81 return defer.succeed(result)
@@ -250,7 +254,7 @@
250 self.patch(backend, "WebClient", MockWebClient)254 self.patch(backend, "WebClient", MockWebClient)
251 self.patch(backend, "dbus_client", MockDBusClient())255 self.patch(backend, "dbus_client", MockDBusClient())
252 self.patch(backend, "replication_client", MockReplicationClient())256 self.patch(backend, "replication_client", MockReplicationClient())
253 self.local_token = "Computer" + TOKEN["token"]257 self.local_token = DEVICE_TYPE_COMPUTER + TOKEN["token"]
254 self.be = backend.ControlBackend()258 self.be = backend.ControlBackend()
255259
256 self.memento = MementoHandler()260 self.memento = MementoHandler()
@@ -278,6 +282,24 @@
278 result = yield self.be.device_is_local(did)282 result = yield self.be.device_is_local(did)
279 self.assertFalse(result)283 self.assertFalse(result)
280284
285 def test_shutdown_func(self):
286 """A shutdown_func can be passed as creation parameter."""
287 f = lambda: None
288 be = backend.ControlBackend(shutdown_func=f)
289 self.assertEqual(be.shutdown_func, f)
290
291 def test_shutdown_func_is_called_on_shutdown(self):
292 """The shutdown_func is called on shutdown."""
293 self.be.shutdown_func = self._set_called
294 self.be.shutdown()
295 self.assertEqual(self._called, ((), {}))
296
297 def test_shutdown_func_when_none(self):
298 """The shutdown_func can be None."""
299 self.be.shutdown_func = None
300 self.be.shutdown()
301 # nothing explodes
302
281303
282class BackendAccountTestCase(BackendBasicTestCase):304class BackendAccountTestCase(BackendBasicTestCase):
283 """Account tests for the backend."""305 """Account tests for the backend."""
@@ -305,7 +327,20 @@
305 """The account_info method exercises its errback."""327 """The account_info method exercises its errback."""
306 # pylint: disable=E1101328 # pylint: disable=E1101
307 self.be.wc.failure = 404329 self.be.wc.failure = 404
308 yield self.assertFailure(self.be.account_info(), WebClientError)330 yield self.assertFailure(self.be.account_info(),
331 backend.WebClientError)
332
333 @inlineCallbacks
334 def test_account_info_fails_with_unauthorized(self):
335 """The account_info clears the credentials on unauthorized."""
336 # pylint: disable=E1101
337 self.be.wc.failure = 401
338 d = defer.Deferred()
339 self.patch(backend.dbus_client, 'clear_credentials',
340 lambda: d.callback('called'))
341 yield self.assertFailure(self.be.account_info(),
342 backend.UnauthorizedError)
343 yield d
309344
310345
311class BackendDevicesTestCase(BackendBasicTestCase):346class BackendDevicesTestCase(BackendBasicTestCase):
@@ -322,14 +357,86 @@
322 @inlineCallbacks357 @inlineCallbacks
323 def test_devices_info_fails(self):358 def test_devices_info_fails(self):
324 """The devices_info method exercises its errback."""359 """The devices_info method exercises its errback."""
360 def fail(*args, **kwargs):
361 """Raise any error other than WebClientError."""
362 raise ValueError(args)
363
364 self.patch(self.be.wc, 'call_api', fail)
365 yield self.assertFailure(self.be.devices_info(), ValueError)
366
367 @inlineCallbacks
368 def test_devices_info_with_webclient_error(self):
369 """The devices_info returns local info if webclient error."""
325 # pylint: disable=E1101370 # pylint: disable=E1101
326 self.be.wc.failure = 404371 self.be.wc.failure = 404
327 yield self.assertFailure(self.be.devices_info(), WebClientError)372 result = yield self.be.devices_info()
373
374 self.assertEqual(result, [LOCAL_DEVICE])
375 self.assertTrue(self.memento.check_error('devices_info',
376 'web client failure'))
377
378 @inlineCallbacks
379 def test_devices_info_fails_with_unauthorized(self):
380 """The devices_info clears the credentials on unauthorized."""
381 # pylint: disable=E1101
382 self.be.wc.failure = 401
383 d = defer.Deferred()
384 self.patch(backend.dbus_client, 'clear_credentials',
385 lambda: d.callback('called'))
386 yield self.assertFailure(self.be.devices_info(),
387 backend.UnauthorizedError)
388 yield d
389
390 @inlineCallbacks
391 def test_devices_info_if_files_disable(self):
392 """The devices_info returns device only info if files is disabled."""
393 yield self.be.disable_files()
394 status = yield self.be.file_sync_status()
395 assert status['status'] == backend.FILE_SYNC_DISABLED, status
396
397 # pylint: disable=E1101
398 self.be.wc.results[DEVICES_API] = SAMPLE_DEVICES_JSON
399 result = yield self.be.devices_info()
400
401 expected = EXPECTED_DEVICES_INFO[:]
402 for device in expected:
403 device.pop('limit_bandwidth', None)
404 device.pop('max_download_speed', None)
405 device.pop('max_upload_speed', None)
406 device.pop('show_all_notifications', None)
407 device['configurable'] = ''
408 self.assertEqual(result, expected)
409
410 @inlineCallbacks
411 def test_devices_info_when_token_name_is_empty(self):
412 """The devices_info can handle empty token names."""
413 # pylint: disable=E1101
414 self.be.wc.results[DEVICES_API] = EMPTY_DESCRIPTION_JSON
415 result = yield self.be.devices_info()
416 expected = {'configurable': '',
417 'device_id': 'ComputerABCDEF01234token',
418 'is_local': '', 'name': 'None',
419 'type': DEVICE_TYPE_COMPUTER}
420 self.assertEqual(result, [expected])
421 self.assertTrue(self.memento.check_warning('name', 'None'))
422
423 @inlineCallbacks
424 def test_devices_info_does_not_log_device_id(self):
425 """The devices_info does not log the device_id."""
426 # pylint: disable=E1101
427 self.be.wc.results[DEVICES_API] = SAMPLE_DEVICES_JSON
428 yield self.be.devices_info()
429
430 dids = (d['device_id'] for d in EXPECTED_DEVICES_INFO)
431 device_id_logged = all(all(did not in r.getMessage()
432 for r in self.memento.records)
433 for did in dids)
434 self.assertTrue(device_id_logged)
328435
329 @inlineCallbacks436 @inlineCallbacks
330 def test_remove_device(self):437 def test_remove_device(self):
331 """The remove_device method calls the right api."""438 """The remove_device method calls the right api."""
332 dtype, did = "Computer", "SAMPLE-TOKEN"439 dtype, did = DEVICE_TYPE_COMPUTER, "SAMPLE-TOKEN"
333 device_id = dtype + did440 device_id = dtype + did
334 apiurl = DEVICE_REMOVE_API % (dtype.lower(), did)441 apiurl = DEVICE_REMOVE_API % (dtype.lower(), did)
335 # pylint: disable=E1101442 # pylint: disable=E1101
@@ -354,7 +461,30 @@
354 """The remove_device method fails as expected."""461 """The remove_device method fails as expected."""
355 # pylint: disable=E1101462 # pylint: disable=E1101
356 self.be.wc.failure = 404463 self.be.wc.failure = 404
357 yield self.assertFailure(self.be.devices_info(), WebClientError)464 yield self.assertFailure(self.be.remove_device(self.local_token),
465 backend.WebClientError)
466
467 @inlineCallbacks
468 def test_remove_device_fails_with_unauthorized(self):
469 """The remove_device clears the credentials on unauthorized."""
470 # pylint: disable=E1101
471 self.be.wc.failure = 401
472 d = defer.Deferred()
473 self.patch(backend.dbus_client, 'clear_credentials',
474 lambda: d.callback('called'))
475 yield self.assertFailure(self.be.remove_device(self.local_token),
476 backend.UnauthorizedError)
477 yield d
478
479 @inlineCallbacks
480 def test_remove_device_does_not_log_device_id(self):
481 """The remove_device does not log the device_id."""
482 device_id = DEVICE_TYPE_COMPUTER + TOKEN['token']
483 yield self.be.remove_device(device_id)
484
485 device_id_logged = all(device_id not in r.getMessage()
486 for r in self.memento.records)
487 self.assertTrue(device_id_logged)
358488
359 @inlineCallbacks489 @inlineCallbacks
360 def test_change_show_all_notifications(self):490 def test_change_show_all_notifications(self):
@@ -411,6 +541,16 @@
411 self.assertEqual(backend.dbus_client.limits["upload"], -1)541 self.assertEqual(backend.dbus_client.limits["upload"], -1)
412 self.assertEqual(backend.dbus_client.limits["download"], -1)542 self.assertEqual(backend.dbus_client.limits["download"], -1)
413543
544 @inlineCallbacks
545 def test_changing_settings_does_not_log_device_id(self):
546 """The change_device_settings does not log the device_id."""
547 device_id = 'yadda-yadda'
548 yield self.be.change_device_settings(device_id, {})
549
550 device_id_logged = all(device_id not in r.getMessage()
551 for r in self.memento.records)
552 self.assertTrue(device_id_logged)
553
414554
415class BackendVolumesTestCase(BackendBasicTestCase):555class BackendVolumesTestCase(BackendBasicTestCase):
416 """Volumes tests for the backend."""556 """Volumes tests for the backend."""
@@ -581,6 +721,12 @@
581class BackendSyncStatusTestCase(BackendBasicTestCase):721class BackendSyncStatusTestCase(BackendBasicTestCase):
582 """Syncdaemon state for the backend."""722 """Syncdaemon state for the backend."""
583723
724 was_disabled = False
725
726 def setUp(self):
727 super(BackendSyncStatusTestCase, self).setUp()
728 self.be.file_sync_disabled = self.was_disabled
729
584 def _build_msg(self):730 def _build_msg(self):
585 """Build expected message regarding file sync status."""731 """Build expected message regarding file sync status."""
586 return '%s (%s)' % (MockDBusClient.status['description'],732 return '%s (%s)' % (MockDBusClient.status['description'],
@@ -599,6 +745,7 @@
599 """The syncdaemon status is processed and emitted."""745 """The syncdaemon status is processed and emitted."""
600 self.patch(MockDBusClient, 'file_sync', False)746 self.patch(MockDBusClient, 'file_sync', False)
601 yield self.assert_correct_status(FILE_SYNC_DISABLED, msg='')747 yield self.assert_correct_status(FILE_SYNC_DISABLED, msg='')
748 self.assertTrue(self.be.file_sync_disabled)
602749
603 @inlineCallbacks750 @inlineCallbacks
604 def test_error(self):751 def test_error(self):
@@ -610,6 +757,8 @@
610 'description': 'auth failed',757 'description': 'auth failed',
611 }758 }
612 yield self.assert_correct_status(FILE_SYNC_ERROR)759 yield self.assert_correct_status(FILE_SYNC_ERROR)
760 # self.be.file_sync_disabled does not change
761 self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
613762
614 @inlineCallbacks763 @inlineCallbacks
615 def test_starting_when_init_not_user(self):764 def test_starting_when_init_not_user(self):
@@ -620,6 +769,7 @@
620 'name': 'INIT', 'description': 'something new',769 'name': 'INIT', 'description': 'something new',
621 }770 }
622 yield self.assert_correct_status(FILE_SYNC_STARTING)771 yield self.assert_correct_status(FILE_SYNC_STARTING)
772 self.assertFalse(self.be.file_sync_disabled)
623773
624 @inlineCallbacks774 @inlineCallbacks
625 def test_starting_when_init_with_user(self):775 def test_starting_when_init_with_user(self):
@@ -630,6 +780,7 @@
630 'name': 'INIT', 'description': 'something new',780 'name': 'INIT', 'description': 'something new',
631 }781 }
632 yield self.assert_correct_status(FILE_SYNC_STARTING)782 yield self.assert_correct_status(FILE_SYNC_STARTING)
783 self.assertFalse(self.be.file_sync_disabled)
633784
634 @inlineCallbacks785 @inlineCallbacks
635 def test_starting_when_local_rescan_not_user(self):786 def test_starting_when_local_rescan_not_user(self):
@@ -640,6 +791,7 @@
640 'name': 'LOCAL_RESCAN', 'description': 'something new',791 'name': 'LOCAL_RESCAN', 'description': 'something new',
641 }792 }
642 yield self.assert_correct_status(FILE_SYNC_STARTING)793 yield self.assert_correct_status(FILE_SYNC_STARTING)
794 self.assertFalse(self.be.file_sync_disabled)
643795
644 @inlineCallbacks796 @inlineCallbacks
645 def test_starting_when_local_rescan_with_user(self):797 def test_starting_when_local_rescan_with_user(self):
@@ -650,6 +802,7 @@
650 'name': 'LOCAL_RESCAN', 'description': 'something new',802 'name': 'LOCAL_RESCAN', 'description': 'something new',
651 }803 }
652 yield self.assert_correct_status(FILE_SYNC_STARTING)804 yield self.assert_correct_status(FILE_SYNC_STARTING)
805 self.assertFalse(self.be.file_sync_disabled)
653806
654 @inlineCallbacks807 @inlineCallbacks
655 def test_starting_when_ready_with_user(self):808 def test_starting_when_ready_with_user(self):
@@ -660,6 +813,7 @@
660 'name': 'READY', 'description': 'something nicer',813 'name': 'READY', 'description': 'something nicer',
661 }814 }
662 yield self.assert_correct_status(FILE_SYNC_STARTING)815 yield self.assert_correct_status(FILE_SYNC_STARTING)
816 self.assertFalse(self.be.file_sync_disabled)
663817
664 @inlineCallbacks818 @inlineCallbacks
665 def test_disconnected(self):819 def test_disconnected(self):
@@ -672,6 +826,9 @@
672 }826 }
673 yield self.assert_correct_status(FILE_SYNC_DISCONNECTED)827 yield self.assert_correct_status(FILE_SYNC_DISCONNECTED)
674828
829 # self.be.file_sync_disabled does not change
830 self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
831
675 @inlineCallbacks832 @inlineCallbacks
676 def test_disconnected_when_waiting(self):833 def test_disconnected_when_waiting(self):
677 """The syncdaemon status is processed and emitted."""834 """The syncdaemon status is processed and emitted."""
@@ -682,6 +839,9 @@
682 }839 }
683 yield self.assert_correct_status(FILE_SYNC_DISCONNECTED)840 yield self.assert_correct_status(FILE_SYNC_DISCONNECTED)
684841
842 # self.be.file_sync_disabled does not change
843 self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
844
685 @inlineCallbacks845 @inlineCallbacks
686 def test_syncing_if_online(self):846 def test_syncing_if_online(self):
687 """The syncdaemon status is processed and emitted."""847 """The syncdaemon status is processed and emitted."""
@@ -693,6 +853,9 @@
693 }853 }
694 yield self.assert_correct_status(FILE_SYNC_SYNCING)854 yield self.assert_correct_status(FILE_SYNC_SYNCING)
695855
856 # self.be.file_sync_disabled does not change
857 self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
858
696 @inlineCallbacks859 @inlineCallbacks
697 def test_syncing_even_if_not_online(self):860 def test_syncing_even_if_not_online(self):
698 """The syncdaemon status is processed and emitted."""861 """The syncdaemon status is processed and emitted."""
@@ -704,6 +867,9 @@
704 }867 }
705 yield self.assert_correct_status(FILE_SYNC_SYNCING)868 yield self.assert_correct_status(FILE_SYNC_SYNCING)
706869
870 # self.be.file_sync_disabled does not change
871 self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
872
707 @inlineCallbacks873 @inlineCallbacks
708 def test_idle(self):874 def test_idle(self):
709 """The syncdaemon status is processed and emitted."""875 """The syncdaemon status is processed and emitted."""
@@ -715,6 +881,9 @@
715 }881 }
716 yield self.assert_correct_status(FILE_SYNC_IDLE)882 yield self.assert_correct_status(FILE_SYNC_IDLE)
717883
884 # self.be.file_sync_disabled does not change
885 self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
886
718 @inlineCallbacks887 @inlineCallbacks
719 def test_stopped(self):888 def test_stopped(self):
720 """The syncdaemon status is processed and emitted."""889 """The syncdaemon status is processed and emitted."""
@@ -726,6 +895,9 @@
726 }895 }
727 yield self.assert_correct_status(FILE_SYNC_STOPPED)896 yield self.assert_correct_status(FILE_SYNC_STOPPED)
728897
898 # self.be.file_sync_disabled does not change
899 self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
900
729 @inlineCallbacks901 @inlineCallbacks
730 def test_unknown(self):902 def test_unknown(self):
731 """The syncdaemon status is processed and emitted."""903 """The syncdaemon status is processed and emitted."""
@@ -740,6 +912,9 @@
740 repr(MockDBusClient.status))912 repr(MockDBusClient.status))
741 self.assertTrue(has_warning)913 self.assertTrue(has_warning)
742914
915 # self.be.file_sync_disabled does not change
916 self.assertEqual(self.was_disabled, self.be.file_sync_disabled)
917
743 def test_status_changed(self):918 def test_status_changed(self):
744 """The file_sync_status is the status changed handler."""919 """The file_sync_status is the status changed handler."""
745 self.be.status_changed_handler = self._set_called920 self.be.status_changed_handler = self._set_called
@@ -754,6 +929,21 @@
754 self.assertEqual(self._called, ((expected_status,), {}))929 self.assertEqual(self._called, ((expected_status,), {}))
755930
756931
932class BackendSyncStatusIfDisabledTestCase(BackendSyncStatusTestCase):
933 """Syncdaemon state for the backend when file sync is disabled."""
934
935 was_disabled = True
936
937 @inlineCallbacks
938 def assert_correct_status(self, status, msg=None):
939 """Check that the resulting status is correct."""
940 sup = super(BackendSyncStatusIfDisabledTestCase, self)
941 if status != FILE_SYNC_STARTING:
942 yield sup.assert_correct_status(FILE_SYNC_DISABLED, msg='')
943 else:
944 yield sup.assert_correct_status(status, msg=msg)
945
946
757class BackendFileSyncOpsTestCase(BackendBasicTestCase):947class BackendFileSyncOpsTestCase(BackendBasicTestCase):
758 """Syncdaemon operations for the backend."""948 """Syncdaemon operations for the backend."""
759949
@@ -768,6 +958,7 @@
768958
769 yield self.be.enable_files()959 yield self.be.enable_files()
770 self.assertTrue(MockDBusClient.file_sync)960 self.assertTrue(MockDBusClient.file_sync)
961 self.assertFalse(self.be.file_sync_disabled)
771962
772 @inlineCallbacks963 @inlineCallbacks
773 def test_disable_files(self):964 def test_disable_files(self):
@@ -776,6 +967,7 @@
776967
777 yield self.be.disable_files()968 yield self.be.disable_files()
778 self.assertFalse(MockDBusClient.file_sync)969 self.assertFalse(MockDBusClient.file_sync)
970 self.assertTrue(self.be.file_sync_disabled)
779971
780 @inlineCallbacks972 @inlineCallbacks
781 def test_connect_files(self):973 def test_connect_files(self):
@@ -783,6 +975,7 @@
783 yield self.be.connect_files()975 yield self.be.connect_files()
784976
785 self.assertEqual(MockDBusClient.actions, ['connect'])977 self.assertEqual(MockDBusClient.actions, ['connect'])
978 self.assertFalse(self.be.file_sync_disabled)
786979
787 @inlineCallbacks980 @inlineCallbacks
788 def test_disconnect_files(self):981 def test_disconnect_files(self):
@@ -790,6 +983,7 @@
790 yield self.be.disconnect_files()983 yield self.be.disconnect_files()
791984
792 self.assertEqual(MockDBusClient.actions, ['disconnect'])985 self.assertEqual(MockDBusClient.actions, ['disconnect'])
986 self.assertFalse(self.be.file_sync_disabled)
793987
794 @inlineCallbacks988 @inlineCallbacks
795 def test_restart_files(self):989 def test_restart_files(self):
@@ -797,6 +991,7 @@
797 yield self.be.restart_files()991 yield self.be.restart_files()
798992
799 self.assertEqual(MockDBusClient.actions, ['stop', 'start'])993 self.assertEqual(MockDBusClient.actions, ['stop', 'start'])
994 self.assertFalse(self.be.file_sync_disabled)
800995
801 @inlineCallbacks996 @inlineCallbacks
802 def test_start_files(self):997 def test_start_files(self):
@@ -804,6 +999,7 @@
804 yield self.be.start_files()999 yield self.be.start_files()
8051000
806 self.assertEqual(MockDBusClient.actions, ['start'])1001 self.assertEqual(MockDBusClient.actions, ['start'])
1002 self.assertFalse(self.be.file_sync_disabled)
8071003
808 @inlineCallbacks1004 @inlineCallbacks
809 def test_stop_files(self):1005 def test_stop_files(self):
@@ -811,6 +1007,7 @@
811 yield self.be.stop_files()1007 yield self.be.stop_files()
8121008
813 self.assertEqual(MockDBusClient.actions, ['stop'])1009 self.assertEqual(MockDBusClient.actions, ['stop'])
1010 self.assertFalse(self.be.file_sync_disabled)
8141011
8151012
816class BackendReplicationsTestCase(BackendBasicTestCase):1013class BackendReplicationsTestCase(BackendBasicTestCase):
8171014
=== modified file 'ubuntuone/controlpanel/utils.py'
--- ubuntuone/controlpanel/utils.py 2010-12-22 13:33:25 +0000
+++ ubuntuone/controlpanel/utils.py 2011-04-08 19:43:13 +0000
@@ -52,7 +52,7 @@
5252
53 # otherwise, try to load PROJECT_DIR from installation path53 # otherwise, try to load PROJECT_DIR from installation path
54 try:54 try:
55 # pylint: disable=F0401, E061155 # pylint: disable=F0401, E0611, W0404
56 from ubuntuone.controlpanel.constants import PROJECT_DIR56 from ubuntuone.controlpanel.constants import PROJECT_DIR
57 return PROJECT_DIR57 return PROJECT_DIR
58 except ImportError:58 except ImportError:
5959
=== modified file 'ubuntuone/controlpanel/webclient.py'
--- ubuntuone/controlpanel/webclient.py 2010-12-22 13:33:25 +0000
+++ ubuntuone/controlpanel/webclient.py 2011-04-08 19:43:13 +0000
@@ -32,10 +32,18 @@
32logger = setup_logging('webclient')32logger = setup_logging('webclient')
3333
3434
35# full list of status codes
36# http://library.gnome.org/devel/libsoup/stable/libsoup-2.4-soup-status.html
37
38
35class WebClientError(Exception):39class WebClientError(Exception):
36 """An http error happened while calling the webservice."""40 """An http error happened while calling the webservice."""
3741
3842
43class UnauthorizedError(WebClientError):
44 """The request ended with bad_request, unauthorized or forbidden."""
45
46
39class WebClient(object):47class WebClient(object):
40 """A client for the u1 webservice."""48 """A client for the u1 webservice."""
4149
@@ -48,21 +56,24 @@
4856
49 def _handler(self, session, msg, d):57 def _handler(self, session, msg, d):
50 """Handle the result of an http message."""58 """Handle the result of an http message."""
51 logger.debug("WebClient: got http response %d for uri %r",59 logger.debug("got http response %d for uri %r",
52 msg.status_code, msg.get_uri().to_string(False))60 msg.status_code, msg.get_uri().to_string(False))
53 data = msg.response_body.data61 data = msg.response_body.data
54 if msg.status_code == 200:62 if msg.status_code == 200:
55 result = simplejson.loads(data)63 result = simplejson.loads(data)
56 d.callback(result)64 d.callback(result)
57 else:65 else:
58 e = WebClientError(msg.status_code, data)66 if msg.status_code in (401,):
67 e = UnauthorizedError(msg.status_code, data)
68 else:
69 e = WebClientError(msg.status_code, data)
59 d.errback(e)70 d.errback(e)
6071
61 def _call_api_with_creds(self, credentials, api_name):72 def _call_api_with_creds(self, credentials, api_name):
62 """Get a given url from the webservice with credentials."""73 """Get a given url from the webservice with credentials."""
63 url = (self.base_url + api_name).encode('utf-8')74 url = (self.base_url + api_name).encode('utf-8')
64 method = "GET"75 method = "GET"
65 logger.debug("WebClient: getting url: %s, %s", method, url)76 logger.debug("getting url: %s, %s", method, url)
66 msg = Soup.Message.new(method, url)77 msg = Soup.Message.new(method, url)
67 add_oauth_headers(msg.request_headers.append, method, url, credentials)78 add_oauth_headers(msg.request_headers.append, method, url, credentials)
68 d = defer.Deferred()79 d = defer.Deferred()
@@ -71,7 +82,8 @@
7182
72 def call_api(self, api_name):83 def call_api(self, api_name):
73 """Get a given url from the webservice."""84 """Get a given url from the webservice."""
74 logger.debug("WebClient: calling api: %s", api_name)85 # this may log device ID's, but only for removals, which is OK
86 logger.debug("calling api: %s", api_name)
75 d = self.get_credentials()87 d = self.get_credentials()
76 d.addCallback(self._call_api_with_creds, api_name)88 d.addCallback(self._call_api_with_creds, api_name)
77 return d89 return d

Subscribers

People subscribed via source and target branches