Merge lp:~chipaca/ubuntuone-client/devices-tab into lp:ubuntuone-client

Proposed by John Lenton
Status: Merged
Approved by: dobey
Approved revision: 409
Merged at revision: not available
Proposed branch: lp:~chipaca/ubuntuone-client/devices-tab
Merge into: lp:ubuntuone-client
Diff against target: 924 lines (+521/-216)
3 files modified
bin/ubuntuone-preferences (+378/-163)
tests/test_preferences.py (+141/-53)
ubuntuone/syncdaemon/dbus_interface.py (+2/-0)
To merge this branch: bzr merge lp:~chipaca/ubuntuone-client/devices-tab
Reviewer Review Type Date Requested Status
dobey (community) Approve
Eric Casteleijn (community) Approve
Natalia Bidart (community) Approve
Review via email: mp+20764@code.launchpad.net

This proposal supersedes a proposal from 2010-03-03.

Commit message

Update the devices tab UI to show the list of devices and allow removing them.

Description of the change

second try, now with pasing tests, heh

To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) wrote : Posted in a previous version of this proposal

Getting keyring errors

Revision history for this message
Eric Casteleijn (thisfred) wrote : Posted in a previous version of this proposal
Download full text (8.5 KiB)

===============================================================================
[ERROR]: tests.test_preferences.PreferencesTests.test_bw_throttling

Traceback (most recent call last):
  File "/home/eric/canonical/ubuntuone-client/devices-tab/contrib/mocker.py", line 102, in test_method_wrapper
    result = test_method()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/tests/test_preferences.py", line 96, in test_bw_throttling
    dialog = self.u1prefs.UbuntuOneDialog()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 473, in __init__
    self.__construct()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 686, in __construct
    self.devices.get_devices()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 258, in get_devices
    token = get_access_token()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 84, in get_access_token
    'oauth-consumer-key': 'ubuntuone'})
gnomekeyring.IOError:
===============================================================================
[ERROR]: tests.test_preferences.PreferencesTests.test_quota_display

Traceback (most recent call last):
  File "/home/eric/canonical/ubuntuone-client/devices-tab/contrib/mocker.py", line 102, in test_method_wrapper
    result = test_method()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/tests/test_preferences.py", line 107, in test_quota_display
    dialog = self.u1prefs.UbuntuOneDialog()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 473, in __init__
    self.__construct()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 686, in __construct
    self.devices.get_devices()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 258, in get_devices
    token = get_access_token()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 84, in get_access_token
    'oauth-consumer-key': 'ubuntuone'})
gnomekeyring.IOError:
===============================================================================
[ERROR]: tests.test_preferences.PreferencesTests.test_request_account_info

Traceback (most recent call last):
  File "/home/eric/canonical/ubuntuone-client/devices-tab/contrib/mocker.py", line 102, in test_method_wrapper
    result = test_method()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/tests/test_preferences.py", line 135, in test_request_account_info
    dialog = self.u1prefs.UbuntuOneDialog()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 473, in __init__
    self.__construct()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 686, in __construct
    self.devices.get_devices()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 258, in get_devices
    token = get_access_token()
  File "/home/eric/canonical/ubuntuone-client/devices-tab/bin/ubuntuone-preferences", line 84, in get_access_token
    'oauth-consumer...

Read more...

review: Needs Fixing
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Test pass though I'm getting this in the console:

nessita@dali:~/canonical/ubuntuone-client/review_devices-tab$ ./bin/ubuntuone-preferences
<twisted.python.failure.Failure <class 'dbus.exceptions.DBusException'>>
DBusException(dbus.String(u'Message did not receive a reply (timeout by message bus)'),)
DBusException(dbus.String(u'Message did not receive a reply (timeout by message bus)'),)

And the same error appears on the 'Devices' tab.
Please also note that the toplevel window changes size in some not-nice way to be able to display the long label with the legend "Error: org.freedesktop.DBus.Error.NoReply: Message ..."

Note that my SD won't start due to metadata corruption. I'll re-test once my SD is working.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

> Please also note that the toplevel window changes size in some not-nice way to
> be able to display the long label with the legend "Error:
> org.freedesktop.DBus.Error.NoReply: Message ..."

As per Chipaca request:

(03:35:47 PM) Chipaca: nessita: what should I show instead?
(03:37:01 PM) nessita: Chipaca: "Sweet end user, we think you may have a problem with the underlying synching service. Please contact <bla> at <bla> or look for some online help <here>. We love you, we want to help you, please don't switch to dropbox"

406. By John Lenton

worked around a bug in the rest api

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Approving. I opened Bug #532841 for the "ugly" error thingy.

Small fixes for docstrings:

 * First line should be next to the double-triple quotes.
 * First line should be a one-liner follow up by a blank line.
 * Line 183 says "dialo."
 * Every line should be ended with a dot.

review: Approve
Revision history for this message
dobey (dobey) wrote :

86 + self.status_label = gtk.Label("Please wait...")
87 + self.attach(self.status_label, 0, 3, 2, 3)
88 +
89 + self.description = gtk.Label("The devices connected to with your personal cloud network are listed below")
90 + self.description.set_alignment(0., .5)
91 + self.description.set_line_wrap(True)
92 + self.attach(self.description, 0, 3, 0, 1, xpadding=12, ypadding=12)

These need to be translated if they're going to be in here. However, I'd prefer to just remove them. "Please wait" should very rarely, if ever, be seen, and "The devices..." is redundant and made obvious by the contents of the tab.

245 + response = response.read() # neither shouold this

Typo.

303 + butn = gtk.Button('Remove')

Needs to be marked for translation.

330 + self.conn_btn = gtk.Button(_('_Connect'))

This creates a mnemonic key which conflicts with the _Close button of the dialog. I think we should perhaps avoid having mnemonics on the Connect/Disconnect and Restart buttons. But we should probably set them to _U and _D for the Upload/Download labels for the spin buttons (and hook the mnemonics to the widgets), as was previously done...

581 - label = gtk.Label(_("Maximum _upload speed (KB/s):"))
596 - label = gtk.Label(_("Maximum _download speed (KB/s):"))
311 + up_lbl = gtk.Label(_("Upload (kB/s):"))
318 + dn_lbl = gtk.Label(_("Download (kB/s):"))

Any reason to change these labels from what they were previously? And to change from KB to kB? This breaks the existing translations.

Also, since you added logging in this branch, is there any reason you don't log the errors when they occur, but only stick them in a status label?

review: Needs Fixing
Revision history for this message
Eric Casteleijn (thisfred) wrote :

All tests pass now.

review: Approve
407. By John Lenton

fixed issues raised by dobey

Revision history for this message
John Lenton (chipaca) wrote :

> Approving. I opened Bug #532841 for the "ugly" error thingy.
>
> Small fixes for docstrings:
>
> * First line should be next to the double-triple quotes.

I disagree; it looks ugly in the code, except for docstrings that are just one line, like

   """A docstring in a single line"""

although even there I think consistency with multi-line docstrings, plus the added readability due to the extra whitespace, makes the other form better.

WRT consistency with the PEP, PEP 257 says «The summary line may be on the same line as the opening quotes or on the next line.»

> * First line should be a one-liner follow up by a blank line.

yep, edited those down a little.

> * Line 183 says "dialo."

good catch!

> * Every line should be ended with a dot.

ok.

Revision history for this message
John Lenton (chipaca) wrote :

> 86 + self.status_label = gtk.Label("Please wait...")
> 87 + self.attach(self.status_label, 0, 3, 2, 3)
> 88 +
> 89 + self.description = gtk.Label("The devices connected to with your
> personal cloud network are listed below")
> 90 + self.description.set_alignment(0., .5)
> 91 + self.description.set_line_wrap(True)
> 92 + self.attach(self.description, 0, 3, 0, 1, xpadding=12, ypadding=12)
>
> These need to be translated if they're going to be in here. However, I'd
> prefer to just remove them. "Please wait" should very rarely, if ever, be
> seen, and "The devices..." is redundant and made obvious by the contents of
> the tab.
>
> 245 + response = response.read() # neither shouold this
>
> Typo.
>
> 303 + butn = gtk.Button('Remove')
>
> Needs to be marked for translation.
>
> 330 + self.conn_btn = gtk.Button(_('_Connect'))
>
> This creates a mnemonic key which conflicts with the _Close button of the
> dialog. I think we should perhaps avoid having mnemonics on the
> Connect/Disconnect and Restart buttons. But we should probably set them to _U
> and _D for the Upload/Download labels for the spin buttons (and hook the
> mnemonics to the widgets), as was previously done...
>
> 581 - label = gtk.Label(_("Maximum _upload speed (KB/s):"))
> 596 - label = gtk.Label(_("Maximum _download speed (KB/s):"))
> 311 + up_lbl = gtk.Label(_("Upload (kB/s):"))
> 318 + dn_lbl = gtk.Label(_("Download (kB/s):"))
>
> Any reason to change these labels from what they were previously? And to
> change from KB to kB? This breaks the existing translations.
>
> Also, since you added logging in this branch, is there any reason you don't
> log the errors when they occur, but only stick them in a status label?

I believe I've fixed all these (I left the "the devices connected ..." in because I think it's not as clear as you think it is :). Could you re-review? Tks.

408. By John Lenton

fixed issues raised by nessa

Revision history for this message
dobey (dobey) wrote :

Can we get rid of the '<b>Name</b>' label and row? I think it's a waste; it's oddly positioned, and looks weird.

The _Connect and _Disconnect labels still conflict with other mnemonics in the dialog. Please get rid of the _ in the strings, or change them to be _o in both strings (so that it doesn't change mnemonics).

You didn't add any more logger.error() calls. Please use logger.error() to log the errors you trap from DBus or elsewhere.

And I still thoroughly disagree with the "The devices blah blah" label. :)

review: Needs Fixing
Revision history for this message
John Lenton (chipaca) wrote :

> Can we get rid of the '<b>Name</b>' label and row? I think it's a waste; it's
> oddly positioned, and looks weird.

darn it, you're right. Bye bye, <b>Name</b>.

> The _Connect and _Disconnect labels still conflict with other mnemonics in the
> dialog. Please get rid of the _ in the strings, or change them to be _o in
> both strings (so that it doesn't change mnemonics).

oops, I forgot this one. Done.

> You didn't add any more logger.error() calls. Please use logger.error() to log
> the errors you trap from DBus or elsewhere.

d'oh, I used logging.error instead (let's call it a typo).

> And I still thoroughly disagree with the "The devices blah blah" label. :)

Noted.

409. By John Lenton

more changed by dobey (and some older ones)

Revision history for this message
dobey (dobey) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/ubuntuone-preferences'
2--- bin/ubuntuone-preferences 2010-03-01 22:10:22 +0000
3+++ bin/ubuntuone-preferences 2010-03-08 19:59:28 +0000
4@@ -23,6 +23,7 @@
5 import pygtk
6 pygtk.require('2.0')
7 import gobject
8+import glib
9 import gtk
10 import os
11 import gettext
12@@ -32,6 +33,11 @@
13 from oauth import oauth
14 from ubuntuone import clientdefs
15 from ubuntuone.syncdaemon.tools import SyncDaemonTool
16+from ubuntuone.syncdaemon.logger import LOGFOLDER
17+
18+import logging
19+import sys
20+import httplib, urlparse, socket
21
22 import dbus.service
23 from ConfigParser import ConfigParser
24@@ -39,6 +45,11 @@
25 from dbus.mainloop.glib import DBusGMainLoop
26 from xdg.BaseDirectory import xdg_config_home
27
28+logging.basicConfig(
29+ filename=os.path.join(LOGFOLDER, 'u1-prefs.log'),
30+ level=logging.DEBUG,
31+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
32+logger = logging.getLogger("ubuntuone-preferences")
33 DBusGMainLoop(set_as_default=True)
34
35 _ = gettext.gettext
36@@ -65,10 +76,363 @@
37 pass
38
39
40+def get_access_token(keyring):
41+ items = []
42+ items = keyring.find_items_sync(
43+ keyring.ITEM_GENERIC_SECRET,
44+ {'ubuntuone-realm': "https://ubuntuone.com",
45+ 'oauth-consumer-key': 'ubuntuone'})
46+ secret = items[0].secret
47+ return oauth.OAuthToken.from_string(secret)
48+
49+
50+class DevicesWidget(gtk.Table):
51+ """
52+ the Devices tab.
53+ """
54+ def __init__(self,
55+ bus,
56+ keyring=gnomekeyring,
57+ realm='https://ubuntuone.com',
58+ consumer_key='ubuntuone',
59+ url='https://one.ubuntu.com/api/1.0/devices/'):
60+ super(DevicesWidget, self).__init__(rows=2, columns=3)
61+ self.bus = bus
62+ self.keyring = keyring
63+ self.sdtool = SyncDaemonTool(bus)
64+ self.set_border_width(6)
65+ self.set_row_spacings(6)
66+ self.set_col_spacings(6)
67+ self.devices = None
68+ self.realm = realm
69+ self.consumer_key = consumer_key
70+ self.base_url = url
71+ self.conn = None
72+ self.consumer = None
73+ self.table_widgets = []
74+
75+ self.connected = None # i.e. unknown
76+ self.conn_btn = None
77+ self.up_spin = None
78+ self.dn_spin = None
79+ self.bw_chk = None
80+ self.bw_limited = False
81+ self.up_limit = 2097152
82+ self.dn_limit = 2097152
83+
84+ self._update_id = 0
85+
86+ self.status_label = gtk.Label("")
87+ self.attach(self.status_label, 0, 3, 2, 3)
88+
89+ self.description = gtk.Label(_("The devices connected to with your"
90+ " personal cloud network"
91+ " are listed below"))
92+ self.description.set_alignment(0., .5)
93+ self.description.set_line_wrap(True)
94+ self.attach(self.description, 0, 3, 0, 1, xpadding=12, ypadding=12)
95+
96+ def update_bw_settings(self):
97+ """
98+ Push the bandwidth settings at syncdaemon.
99+ """
100+ try:
101+ client = self.bus.get_object(DBUS_IFACE_NAME, "/config",
102+ follow_name_owner_changes=True)
103+ iface = dbus.Interface(client, DBUS_IFACE_CONFIG_NAME)
104+ iface.set_throttling_limits(self.dn_limit, self.up_limit,
105+ reply_handler=dbus_async,
106+ error_handler=self.error)
107+ if self.bw_limited:
108+ iface.enable_bandwidth_throttling(reply_handler=dbus_async,
109+ error_handler=self.error)
110+ else:
111+ iface.disable_bandwidth_throttling(reply_handler=dbus_async,
112+ error_handler=self.error)
113+ except DBusException, e:
114+ self.error(str(e))
115+
116+ def handle_bw_controls_changed(self, *a):
117+ """
118+ Sync the bandwidth throttling model with the view.
119+
120+ Start a timer to sync with syncdaemon too.
121+ """
122+ # Remove the timeout ...
123+ if self._update_id != 0:
124+ gobject.source_remove(self._update_id)
125+
126+ # sync the model ...
127+ self.bw_limited = self.bw_chk.get_active()
128+ self.up_limit = self.up_spin.get_value_as_int() * 1024
129+ self.dn_limit = self.dn_spin.get_value_as_int() * 1024
130+
131+ # ... and add the timeout back
132+ self._update_id = gobject.timeout_add_seconds(
133+ 1, self.update_bw_settings)
134+
135+ def handle_bw_checkbox_toggled(self, checkbox, *widgets):
136+ """
137+ Callback for the bandwidth togglebutton.
138+ """
139+ active = checkbox.get_active()
140+ for widget in widgets:
141+ widget.set_sensitive(active)
142+ self.handle_bw_controls_changed()
143+
144+ def handle_limits(self, limits):
145+ """
146+ Callback for when syncdaemon tells us its throttling limits.
147+ """
148+ self.up_limit = int(limits['upload'])
149+ self.dn_limit = int(limits['download'])
150+ if self.up_spin is not None and self.dn_spin is not None:
151+ self.up_spin.set_value(self.up_limit / 1024)
152+ self.dn_spin.set_value(self.dn_limit / 1024)
153+
154+ def handle_throttling_enabled(self, enabled):
155+ """
156+ Callback for when syncdaemon tells us whether throttling is enabled.
157+ """
158+ self.bw_limited = enabled
159+ if self.bw_chk is not None:
160+ self.bw_chk.set_active(enabled)
161+
162+ def handle_state_change(self, new_state):
163+ """
164+ Callback for when syncdaemon's state changes.
165+ """
166+ if new_state['is_error']:
167+ # this syncdaemon isn't going to connect no more
168+ self.connected = None
169+ else:
170+ self.connected = new_state['is_connected']
171+ if self.conn_btn is not None:
172+ if self.connected:
173+ self.conn_btn.set_label(_("Disconnect"))
174+ else:
175+ self.conn_btn.set_label(_("Connect"))
176+ if self.connected is None:
177+ self.conn_btn.set_sensitive(False)
178+ else:
179+ self.conn_btn.set_sensitive(True)
180+
181+ def error(self, msg):
182+ """
183+ Clear the table and show the error message in its place.
184+
185+ This might be better as an error dialog.
186+ """
187+ self.clear_devices_view()
188+ self.status_label.set_markup("<b>Error:</b> %s" % msg)
189+ logger.error(msg)
190+
191+ def request(self, path='', method='GET'):
192+ """
193+ Helper that makes an oauth-wrapped rest request.
194+
195+ XXX duplication with request_REST_info (but this one should be async).
196+ """
197+ url = self.base_url + path
198+
199+ token = get_access_token(self.keyring)
200+
201+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(
202+ http_url=url,
203+ http_method=method,
204+ oauth_consumer=self.consumer,
205+ token=token,
206+ parameters='')
207+ oauth_request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
208+ self.consumer, token)
209+
210+ scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
211+
212+ conn = httplib.HTTPSConnection(netloc)
213+ try:
214+ conn.request(method, path, headers=oauth_request.to_header())
215+ except socket.error:
216+ return None
217+ return conn
218+
219+ def get_devices(self):
220+ """
221+ Ask the server for a list of devices
222+
223+ Hook up parse_devices to run on the result (when it gets here).
224+ """
225+ try:
226+ token = get_access_token(self.keyring)
227+ except gnomekeyring.NoMatchError:
228+ self.error("No token in the keyring")
229+ self.devices = []
230+ else:
231+ self.consumer = oauth.OAuthConsumer("ubuntuone", "hammertime")
232+
233+ self.conn = self.request()
234+ if self.conn is None:
235+ self.clear_devices_view()
236+ self.error('Unable to connect')
237+ else:
238+ glib.io_add_watch(
239+ self.conn.sock,
240+ glib.IO_IN | glib.IO_PRI | glib.IO_ERR | glib.IO_HUP,
241+ self.parse_devices)
242+
243+ def parse_devices(self, *a):
244+ """
245+ Parse the list of devices, and hook up list_devices if it worked.
246+ """
247+ response = self.conn.getresponse() # shouldn't block
248+ if response.status == 200:
249+ response = response.read() # neither should this
250+ self.devices = simplejson.loads(response)
251+ gobject.idle_add(self.list_devices)
252+ else:
253+ self.clear_devices_view()
254+ self.error(response.reason)
255+ return False
256+
257+ def clear_devices_view(self):
258+ """
259+ Clear out almost all the widgets.
260+
261+ All except from the table, the description and the
262+ status_label get destroyed.
263+ """
264+ for i in self.get_children():
265+ if i not in (self.description, self.status_label):
266+ i.destroy()
267+ self.conn_btn = None
268+ self.up_spin = None
269+ self.dn_spin = None
270+ self.bw_chk = None
271+
272+ def list_devices(self):
273+ """
274+ Populate the table with the list of devices.
275+
276+ If the list of devices is empty, make a fake one that refers
277+ to the local machine (to get the connect/restart buttons).
278+ """
279+ self.resize(len(self.devices)+1, 3)
280+
281+ self.clear_devices_view()
282+
283+ token = get_access_token(self.keyring)
284+
285+ if not self.devices:
286+ # a stopgap device so you can at least try to connect
287+ self.devices = [{'kind': 'Computer',
288+ 'description': _("<LOCAL MACHINE>"),
289+ 'token': token.key,
290+ 'FAKE': 'YES'}]
291+
292+ self.status_label.set_label("")
293+
294+ i = 0
295+ for row in self.devices:
296+ i += 1
297+ img = gtk.Image()
298+ img.set_from_icon_name(row['kind'].lower(), gtk.ICON_SIZE_DND)
299+ desc = gtk.Label(row['description'])
300+ desc.set_alignment(0., .5)
301+ self.attach(img, 0, 1, i, i+1)
302+ self.attach(desc, 1, 2, i, i+1)
303+ if 'FAKE' not in row:
304+ # we don't include the "Remove" button for the fake entry :)
305+ butn = gtk.Button(_('Remove'))
306+ butn.connect('clicked', self.remove,
307+ row['kind'], row.get('token'))
308+ self.attach(butn, 2, 3, i, i+1, xoptions=0, yoptions=0)
309+ if row.get('token') == token.key:
310+ self.bw_chk = ck_btn = gtk.CheckButton(
311+ _("_Limit Bandwidth Usage"))
312+ ck_btn.set_active(self.bw_limited)
313+ up_lbl = gtk.Label(_("Maximum _upload speed (KB/s):"))
314+ up_lbl.set_alignment(0., .5)
315+ adj = gtk.Adjustment(value=self.up_limit/1024.,
316+ lower=0.0, upper=4096.0,
317+ step_incr=1.0, page_incr=16.0)
318+ self.up_spin = up_btn = gtk.SpinButton(adj)
319+ up_btn.connect("value-changed", self.handle_bw_controls_changed)
320+ up_lbl.set_mnemonic_widget(up_btn)
321+ dn_lbl = gtk.Label(_("Maximum _download speed (KB/s):"))
322+ dn_lbl.set_alignment(0., .5)
323+ adj = gtk.Adjustment(value=self.dn_limit/1024.,
324+ lower=0.0, upper=4096.0,
325+ step_incr=1.0, page_incr=16.0)
326+ self.dn_spin = dn_btn = gtk.SpinButton(adj)
327+ dn_btn.connect("value-changed", self.handle_bw_controls_changed)
328+ dn_lbl.set_mnemonic_widget(dn_btn)
329+ ck_btn.connect('toggled', self.handle_bw_checkbox_toggled,
330+ up_lbl, up_btn, dn_lbl, dn_btn)
331+ self.handle_bw_checkbox_toggled(ck_btn,
332+ up_lbl, up_btn, dn_lbl, dn_btn)
333+
334+ self.conn_btn = gtk.Button(_('Connect'))
335+ if self.connected is None:
336+ self.conn_btn.set_sensitive(False)
337+ elif self.connected:
338+ self.conn_btn.set_label(_('Disconnect'))
339+ self.conn_btn.connect('clicked', self.handle_connect_button)
340+ restart_btn = gtk.Button(_('Restart'))
341+ restart_btn.connect('clicked', self.handle_restart_button)
342+ btn_box = gtk.HButtonBox()
343+ btn_box.add(self.conn_btn)
344+ btn_box.add(restart_btn)
345+
346+ i += 1
347+ self.attach(ck_btn, 1, 3, i, i+1)
348+ i += 1
349+ self.attach(up_lbl, 1, 2, i, i+1)
350+ self.attach(up_btn, 2, 3, i, i+1)
351+ i += 1
352+ self.attach(dn_lbl, 1, 2, i, i+1)
353+ self.attach(dn_btn, 2, 3, i, i+1)
354+ i += 1
355+ self.attach(btn_box, 1, 3, i, i+1)
356+ i += 2
357+ self.show_all()
358+
359+ def handle_connect_button(self, *a):
360+ """
361+ Callback for the Connect/Disconnect button.
362+ """
363+ self.conn_btn.set_sensitive(False)
364+ if self.connected:
365+ d = self.sdtool.disconnect()
366+ else:
367+ d = self.sdtool.connect()
368+
369+ def handle_restart_button(self, *a):
370+ """
371+ Callback for the Restart button.
372+ """
373+ self.sdtool.quit().addCallbacks(lambda _: self.sdtool.start())
374+
375+ def remove(self, button, kind, token):
376+ """
377+ Callback for the Remove button.
378+
379+ Starts an async request to remove a device.
380+ """
381+ self.conn = self.request('remove/%s/%s' % (kind.lower(), token))
382+ if self.conn is None:
383+ self.clear_devices_view()
384+ self.error('Unable to connect')
385+ else:
386+ glib.io_add_watch(
387+ self.conn.sock,
388+ glib.IO_IN | glib.IO_PRI | glib.IO_ERR | glib.IO_HUP,
389+ self.parse_devices)
390+
391+
392+
393 class UbuntuOneDialog(gtk.Dialog):
394 """Preferences dialog."""
395
396- def __init__(self, config=None, *args, **kw):
397+ def __init__(self, config=None, keyring=gnomekeyring, *args, **kw):
398 """Initializes our config dialog."""
399 super(UbuntuOneDialog, self).__init__(*args, **kw)
400 self.set_title(_("Ubuntu One Preferences"))
401@@ -80,12 +444,8 @@
402 self.connect("close", self.__handle_response, gtk.RESPONSE_CLOSE)
403 self.connect("response", self.__handle_response)
404
405- self.bw_enabled = False
406- self.up_limit = 2097152
407- self.dn_limit = 2097152
408-
409 self.__bus = dbus.SessionBus()
410- self.keyring = gnomekeyring
411+ self.keyring = keyring
412
413 self.__bus.add_signal_receiver(
414 handler_function=self.__got_state,
415@@ -108,15 +468,13 @@
416 # Timeout ID to avoid spamming DBus from spinbutton changes
417 self.__update_id = 0
418
419- # Connectivity status
420- self.connected = False
421-
422 # SD Tool object
423 self.sdtool = SyncDaemonTool(self.__bus)
424 self.sdtool.get_status().addCallbacks(lambda _: self.__got_state,
425 self.__sd_error)
426 # Build the dialog
427 self.__construct()
428+ logger.debug("starting")
429
430 def quit(self):
431 """Exit the main loop."""
432@@ -132,97 +490,23 @@
433
434 def __got_state(self, state):
435 """Got the state of syncdaemon."""
436- self.connected = bool(state['is_connected'])
437- if self.connected:
438- self.conn_btn.set_label(_("Disconnect"))
439- else:
440- self.conn_btn.set_label(_("Connect"))
441- self.conn_btn.set_sensitive(True)
442+ self.devices.handle_state_change(state)
443
444 def __got_limits(self, limits):
445 """Got the throttling limits."""
446- self.up_limit = int(limits['upload'])
447- self.dn_limit = int(limits['download'])
448- self.up_spin.set_value(self.up_limit / 1024)
449- self.dn_spin.set_value(self.dn_limit / 1024)
450+ logger.debug("got limits: %s" % (limits,))
451+ self.devices.handle_limits(limits)
452
453 def __got_enabled(self, enabled):
454 """Got the throttling enabled config."""
455- self.bw_enabled = bool(enabled)
456- self.limit_check.set_active(self.bw_enabled)
457-
458- def __update_bw_settings(self):
459- """Update the bandwidth throttling config in syncdaemon."""
460- self.bw_enabled = self.limit_check.get_active()
461- self.up_limit = self.up_spin.get_value_as_int() * 1024
462- self.dn_limit = self.dn_spin.get_value_as_int() * 1024
463-
464- try:
465- client = self.__bus.get_object(DBUS_IFACE_NAME, "/config",
466- follow_name_owner_changes=True)
467- iface = dbus.Interface(client, DBUS_IFACE_CONFIG_NAME)
468- iface.set_throttling_limits(self.dn_limit, self.up_limit,
469- reply_handler=dbus_async,
470- error_handler=self.__dbus_error)
471- if self.bw_enabled:
472- iface.enable_bandwidth_throttling(
473- reply_handler=dbus_async,
474- error_handler=self.__dbus_error)
475- else:
476- iface.disable_bandwidth_throttling(
477- reply_handler=dbus_async,
478- error_handler=self.__dbus_error)
479- except DBusException, e:
480- self.__dbus_error(e)
481+ self.devices.handle_throttling_enabled(enabled)
482
483 def __handle_response(self, dialog, response):
484 """Handle the dialog's response."""
485 self.hide()
486- self.__update_bw_settings()
487+ self.devices.update_bw_settings()
488 gtk.main_quit()
489
490- def __bw_limit_toggled(self, button, data=None):
491- """Toggle the bw limit panel."""
492- self.bw_enabled = self.limit_check.get_active()
493- self.bw_table.set_sensitive(self.bw_enabled)
494- try:
495- client = self.__bus.get_object(DBUS_IFACE_NAME, "/config",
496- follow_name_owner_changes=True)
497- iface = dbus.Interface(client, DBUS_IFACE_CONFIG_NAME)
498- iface.set_throttling_limits(self.dn_limit, self.up_limit,
499- reply_handler=dbus_async,
500- error_handler=self.__dbus_error)
501- if self.bw_enabled:
502- iface.enable_bandwidth_throttling(
503- reply_handler=dbus_async,
504- error_handler=self.__dbus_error)
505- else:
506- iface.disable_bandwidth_throttling(
507- reply_handler=dbus_async,
508- error_handler=self.__dbus_error)
509- except DBusException, e:
510- self.__dbus_error(e)
511-
512- def __spinner_changed(self, button, data=None):
513- """Remove timeout and add anew."""
514- if self.__update_id != 0:
515- gobject.source_remove(self.__update_id)
516-
517- self.__update_id = gobject.timeout_add_seconds(
518- 1, self.__update_bw_settings)
519-
520- def __connect_toggled(self, button, data=None):
521- """Toggle the connection state..."""
522- self.conn_btn.set_sensitive(False)
523- if self.connected:
524- self.sdtool.start().addCallbacks(
525- lambda _: self.sdtool.disconnect(),
526- self.__sd_error)
527- else:
528- self.sdtool.start().addCallbacks(
529- lambda _: self.sdtool.connect(),
530- self.__sd_error)
531-
532 def _format_for_gb_display(self, bytes):
533 """Format bytes into reasonable gb display."""
534 gb = bytes / 1024 / 1024 / 1024
535@@ -247,12 +531,7 @@
536 def request_REST_info(self, url, method):
537 """Make a REST request and return the resulting dict, or None."""
538 consumer = oauth.OAuthConsumer("ubuntuone", "hammertime")
539- items = []
540- items = self.keyring.find_items_sync(
541- gnomekeyring.ITEM_GENERIC_SECRET,
542- {'ubuntuone-realm': "https://ubuntuone.com",
543- 'oauth-consumer-key': consumer.key})
544- token = oauth.OAuthToken.from_string(items[0].secret)
545+ token = get_access_token(self.keyring)
546 request = oauth.OAuthRequest.from_consumer_and_token(
547 http_url=url, http_method=method, oauth_consumer=consumer,
548 token=token)
549@@ -403,75 +682,11 @@
550 self.mail_label.show()
551
552 # Devices tab
553- devices = gtk.VBox(spacing=12)
554- devices.set_border_width(6)
555- self.notebook.append_page(devices)
556- self.notebook.set_tab_label_text(devices, _("Devices"))
557- devices.show()
558-
559- # Bandwidth limiting
560- self.limit_check = gtk.CheckButton(_("_Limit Bandwidth Usage"))
561- self.limit_check.connect("toggled", self.__bw_limit_toggled)
562- devices.pack_start(self.limit_check, False, False)
563- self.limit_check.show()
564-
565- hbox = gtk.HBox(spacing=12)
566- devices.pack_start(hbox, False, False)
567- hbox.show()
568-
569- label = gtk.Label()
570- hbox.pack_start(label, False, False)
571- label.show()
572-
573- rbox = gtk.VBox(spacing=12)
574- hbox.pack_start(rbox, False, False)
575- rbox.show()
576-
577- # Now put the bw limit bits in a table too
578- self.bw_table = gtk.Table(rows=2, columns=2)
579- self.bw_table.set_row_spacings(6)
580- self.bw_table.set_col_spacings(6)
581- self.bw_table.set_sensitive(False)
582- rbox.pack_start(self.bw_table, False, False)
583- self.bw_table.show()
584-
585- # Upload speed
586- label = gtk.Label(_("Maximum _upload speed (KB/s):"))
587- label.set_use_underline(True)
588- label.set_alignment(0, 0.5)
589- self.bw_table.attach(label, 0, 1, 0, 1)
590- label.show()
591-
592- adjustment = gtk.Adjustment(value=2048.0, lower=0.0, upper=4096.0,
593- step_incr=64.0, page_incr=128.0)
594- self.up_spin = gtk.SpinButton(adjustment)
595- self.up_spin.connect("value-changed", self.__spinner_changed)
596- label.set_mnemonic_widget(self.up_spin)
597- self.bw_table.attach(self.up_spin, 1, 2, 0, 1)
598- self.up_spin.show()
599-
600- # Download speed
601- label = gtk.Label(_("Maximum _download speed (KB/s):"))
602- label.set_use_underline(True)
603- label.set_alignment(0, 0.5)
604- self.bw_table.attach(label, 0, 1, 1, 2)
605- label.show()
606- adjustment = gtk.Adjustment(value=2048.0, lower=64.0, upper=8192.0,
607- step_incr=64.0, page_incr=128.0)
608- self.dn_spin = gtk.SpinButton(adjustment)
609- self.dn_spin.connect("value-changed", self.__spinner_changed)
610- label.set_mnemonic_widget(self.dn_spin)
611- self.bw_table.attach(self.dn_spin, 1, 2, 1, 2)
612- self.dn_spin.show()
613-
614- alignment = gtk.Alignment(1.0, 0.5)
615- rbox.pack_end(alignment, False, False)
616- alignment.show()
617-
618- self.conn_btn = gtk.Button(_("Connect"))
619- self.conn_btn.connect('clicked', self.__connect_toggled)
620- alignment.add(self.conn_btn)
621- self.conn_btn.show()
622+ self.devices = DevicesWidget(self.__bus, self.keyring)
623+ self.notebook.append_page(self.devices)
624+ self.notebook.set_tab_label_text(self.devices, _("Devices"))
625+ self.devices.show_all()
626+ self.devices.get_devices()
627
628 # Services tab
629 services = gtk.VBox(spacing=12)
630
631=== modified file 'tests/test_preferences.py'
632--- tests/test_preferences.py 2010-02-18 16:02:23 +0000
633+++ tests/test_preferences.py 2010-03-08 19:59:28 +0000
634@@ -53,26 +53,24 @@
635
636 self.item_id = 999
637
638- ex = self.expect(self.item.item_id)
639- ex.result(self.item_id)
640- ex.count(0, None)
641-
642- ex = self.expect(self.item.secret)
643- ex.result('oauth_token=access_key&oauth_token_secret=access_secret')
644- ex.count(0, None)
645-
646- def expect_token_query(self):
647- """Expects the keyring to be queried for a token."""
648- return self.expect(
649- self.keyring.find_items_sync(
650- gnomekeyring.ITEM_GENERIC_SECRET,
651- {'ubuntuone-realm': 'https://ubuntuone.com',
652- 'oauth-consumer-key': 'ubuntuone'})
653- )
654-
655- def mock_has_token(self):
656- """Mocks a cached token in the keyring."""
657- self.expect_token_query().result([self.item])
658+ self.item.item_id
659+ self.mocker.result(self.item_id)
660+ self.mocker.count(0, None)
661+
662+ self.item.secret
663+ self.mocker.result('oauth_token=access_key'
664+ '&oauth_token_secret=access_secret')
665+ self.mocker.count(0, None)
666+
667+ self.keyring.find_items_sync(
668+ None,
669+ {'ubuntuone-realm': 'https://ubuntuone.com',
670+ 'oauth-consumer-key': 'ubuntuone'})
671+ self.mocker.count(0, None)
672+ self.mocker.result([self.item])
673+ self.keyring.ITEM_GENERIC_SECRET
674+ self.mocker.count(0, None)
675+ self.mocker.result(None)
676
677 def tearDown(self):
678 # collect all signal receivers registered during the test
679@@ -93,18 +91,110 @@
680 def test_bw_throttling(self):
681 """Test that toggling bw throttling works correctly."""
682 self.mocker.replay()
683- dialog = self.u1prefs.UbuntuOneDialog()
684- self.assertTrue(dialog is not None)
685- dialog.notebook.set_current_page(1)
686- self.assertFalse(dialog.bw_table.get_property('sensitive'))
687- dialog.limit_check.set_active(True)
688- self.assertTrue(dialog.bw_table.get_property('sensitive'))
689- dialog.destroy()
690+ widget = self.u1prefs.DevicesWidget(None, keyring=self.keyring)
691+ try:
692+ widget.devices = []
693+ widget.list_devices()
694+ self.assertFalse(widget.bw_limited,
695+ "the bandwidth should start out not limited")
696+ self.assertTrue(widget.bw_chk,
697+ "the checkbox should be present")
698+ self.assertFalse(widget.bw_chk.get_active(),
699+ "the checkbox should start out unchecked")
700+ self.assertFalse(widget.up_spin.get_property('sensitive') or
701+ widget.dn_spin.get_property('sensitive'),
702+ "the spinbuttons should start out unsensitive")
703+ widget.bw_chk.set_active(True)
704+ self.assertTrue(widget.bw_chk.get_active(),
705+ "the checkbox should now be checked")
706+ self.assertTrue(widget.up_spin.get_property('sensitive') and
707+ widget.dn_spin.get_property('sensitive'),
708+ "the spinbuttons should now be sensitive")
709+ finally:
710+ widget.destroy()
711+
712+ def test_list_devices_fills_devices_list_with_fake_result_when_empty(self):
713+ self.mocker.replay()
714+ widget = self.u1prefs.DevicesWidget(None, keyring=self.keyring)
715+ try:
716+ widget.devices = []
717+ widget.list_devices()
718+ # the devices list is no longer empty
719+ self.assertTrue(widget.devices)
720+ # it has 'fake' data (referring to the local machine)
721+ self.assertTrue('FAKE' in widget.devices[0])
722+ finally:
723+ widget.destroy()
724+
725+ def test_list_devices_shows_devices_list(self):
726+ self.mocker.replay()
727+ widget = self.u1prefs.DevicesWidget(None, keyring=self.keyring)
728+ try:
729+ widget.devices = []
730+ widget.list_devices()
731+ # fake data now in devices
732+ interesting = []
733+ for i in widget.get_children():
734+ clsname = i.__class__.__name__
735+ if clsname == 'Image':
736+ interesting.append((clsname, i.get_icon_name()[0]))
737+ if clsname in ('Label', 'Button', 'CheckButton'):
738+ interesting.append((clsname, i.get_label()))
739+ # check there is an image of a computer in there
740+ self.assertTrue(('Image', 'computer') in interesting)
741+ # check a placeholder for the local machine description is there
742+ self.assertTrue(('Label', '<LOCAL MACHINE>') in interesting)
743+ # check the bw limitation stuff is there
744+ self.assertTrue(('CheckButton', '_Limit Bandwidth Usage')
745+ in interesting)
746+ self.assertTrue(('Label', 'Maximum _download speed (KB/s):')
747+ in interesting)
748+ self.assertTrue(('Label', 'Maximum _upload speed (KB/s):')
749+ in interesting)
750+ # check the 'Remove' button is *not* there
751+ self.assertTrue(('Button', 'Remove') not in interesting)
752+ finally:
753+ widget.destroy()
754+
755+ def test_list_devices_shows_real_devices_list(self):
756+ self.mocker.replay()
757+ widget = self.u1prefs.DevicesWidget(None, keyring=self.keyring)
758+ try:
759+ widget.devices = [{'kind': 'Computer',
760+ 'description': 'xyzzy',
761+ 'token': 'blah'},
762+ {'kind': 'Phone',
763+ 'description': 'quux',
764+ 'token': '1234'}]
765+ widget.list_devices()
766+ # fake data now in devices
767+ interesting = []
768+ for i in widget.get_children():
769+ clsname = i.__class__.__name__
770+ if clsname == 'Image':
771+ interesting.append((clsname, i.get_icon_name()[0]))
772+ if clsname in ('Label', 'Button', 'CheckButton'):
773+ interesting.append((clsname, i.get_label()))
774+ # check there is an image of a computer in there
775+ self.assertTrue(('Image', 'computer') in interesting)
776+ # and of a phone
777+ self.assertTrue(('Image', 'phone') in interesting)
778+ # check a label of the local machine description is there
779+ self.assertTrue(('Label', 'xyzzy') in interesting)
780+ # check the bw limitation stuff is not there (no local machine)
781+ self.assertTrue(('CheckButton', '_Limit Bandwidth Usage')
782+ not in interesting)
783+ self.assertTrue(('Label', 'Download (kB/s):') not in interesting)
784+ self.assertTrue(('Label', 'Upload (kB/s):') not in interesting)
785+ # check the 'Remove' button is there
786+ self.assertTrue(('Button', 'Remove') in interesting)
787+ finally:
788+ widget.destroy()
789
790 def test_quota_display(self):
791 """Test that quota display works correctly."""
792 self.mocker.replay()
793- dialog = self.u1prefs.UbuntuOneDialog()
794+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
795 self.assertTrue(dialog is not None)
796 self.assertEqual(dialog.usage_graph.get_fraction(), 0.0)
797 dialog.update_quota_display(1024, 2048)
798@@ -113,11 +203,6 @@
799
800 def test_request_quota_info(self):
801 """Test that we can request the quota info properly."""
802- self.mock_has_token()
803- dialog = self.u1prefs.UbuntuOneDialog()
804- self.assertTrue(dialog is not None)
805- dialog.keyring = self.keyring
806- self.assertEqual(dialog.usage_graph.get_fraction(), 0.0)
807 response = { 'status' : '200' }
808 content = '{"total":2048, "used":1024}'
809 client = self.mocker.mock()
810@@ -125,16 +210,15 @@
811 self.expect(client.request('https://one.ubuntu.com/api/quota/',
812 'GET', KWARGS)).result((response, content))
813 self.mocker.replay()
814+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
815+ self.assertTrue(dialog is not None)
816+ self.assertEqual(dialog.usage_graph.get_fraction(), 0.0)
817 dialog.request_quota_info()
818 self.assertEqual(dialog.usage_graph.get_fraction(), 0.5)
819 dialog.destroy()
820
821 def test_request_account_info(self):
822 """Test that we can request the account info properly."""
823- self.mock_has_token()
824- dialog = self.u1prefs.UbuntuOneDialog()
825- self.assertTrue(dialog is not None)
826- dialog.keyring = self.keyring
827 response = { 'status' : '200' }
828 content = '''{"username": "ubuntuone", "nickname": "Ubuntu One",
829 "email": "uone@example.com"}
830@@ -144,6 +228,8 @@
831 self.expect(client.request('https://one.ubuntu.com/api/account/',
832 'GET', KWARGS)).result((response, content))
833 self.mocker.replay()
834+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
835+ self.assertTrue(dialog is not None)
836 dialog.request_account_info()
837 self.assertEqual(dialog.name_label.get_text(), 'Ubuntu One')
838 self.assertEqual(dialog.user_label.get_text(), 'ubuntuone')
839@@ -152,13 +238,14 @@
840
841 def test_toggle_bookmarks(self):
842 """Test toggling the bookmarks service on/off."""
843- dialog = self.u1prefs.UbuntuOneDialog()
844+ toggle_db_sync = self.mocker.mock()
845+ self.expect(toggle_db_sync('bookmarks', False))
846+ self.expect(toggle_db_sync('bookmarks', True))
847+ self.expect(toggle_db_sync('bookmarks', False))
848+ self.mocker.replay()
849+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
850 self.assertTrue(dialog is not None)
851- dialog.toggle_db_sync = self.mocker.mock()
852- self.expect(dialog.toggle_db_sync('bookmarks', False))
853- self.expect(dialog.toggle_db_sync('bookmarks', True))
854- self.expect(dialog.toggle_db_sync('bookmarks', False))
855- self.mocker.replay()
856+ dialog.toggle_db_sync = toggle_db_sync
857 dialog.bookmarks_check.set_active(True)
858 self.assertTrue(dialog.bookmarks_check.get_active())
859 dialog.bookmarks_check.set_active(False)
860@@ -168,13 +255,14 @@
861
862 def test_toggle_contacts(self):
863 """Test toggling the contacts service on/off."""
864- dialog = self.u1prefs.UbuntuOneDialog()
865+ toggle_db_sync = self.mocker.mock()
866+ self.expect(toggle_db_sync('contacts', False))
867+ self.expect(toggle_db_sync('contacts', True))
868+ self.expect(toggle_db_sync('contacts', False))
869+ self.mocker.replay()
870+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
871 self.assertTrue(dialog is not None)
872- dialog.toggle_db_sync = self.mocker.mock()
873- self.expect(dialog.toggle_db_sync('contacts', False))
874- self.expect(dialog.toggle_db_sync('contacts', True))
875- self.expect(dialog.toggle_db_sync('contacts', False))
876- self.mocker.replay()
877+ dialog.toggle_db_sync = toggle_db_sync
878 dialog.abook_check.set_active(True)
879 self.assertTrue(dialog.abook_check.get_active())
880 dialog.abook_check.set_active(False)
881@@ -184,9 +272,9 @@
882
883 def test_toggle_files(self):
884 """Test toggling the files service on/off."""
885- dialog = self.u1prefs.UbuntuOneDialog()
886+ self.mocker.replay()
887+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
888 self.assertTrue(dialog is not None)
889- self.mocker.replay()
890 dialog.files_check.set_active(True)
891 self.assertTrue(dialog.files_check.get_active())
892 dialog.files_check.set_active(False)
893@@ -196,9 +284,9 @@
894
895 def test_toggle_files_and_music(self):
896 """Test toggling the files and music services on/off."""
897- dialog = self.u1prefs.UbuntuOneDialog()
898+ self.mocker.replay()
899+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
900 self.assertTrue(dialog is not None)
901- self.mocker.replay()
902 dialog.files_check.set_active(False)
903 self.assertFalse(dialog.files_check.get_active())
904 self.assertFalse(dialog.music_check.props.sensitive)
905
906=== modified file 'ubuntuone/syncdaemon/dbus_interface.py'
907--- ubuntuone/syncdaemon/dbus_interface.py 2010-03-05 20:32:42 +0000
908+++ ubuntuone/syncdaemon/dbus_interface.py 2010-03-08 19:59:28 +0000
909@@ -1025,6 +1025,7 @@
910 configured.
911 The values are bytes/second
912 """
913+ logger.debug("called get_throttling_limits")
914 try:
915 aq = self.dbus_iface.action_queue
916 download = -1
917@@ -1052,6 +1053,7 @@
918 def set_throttling_limits(self, download, upload,
919 reply_handler=None, error_handler=None):
920 """Set the read and write limits. The expected values are bytes/sec."""
921+ logger.debug("called set_throttling_limits")
922 try:
923 # modify and save the config file
924 user_config = config.get_user_config()

Subscribers

People subscribed via source and target branches