Merge lp:~ubuntuone-control-tower/ubuntu/lucid/ubuntuone-client/trunk into lp:ubuntu/lucid/ubuntuone-client

Proposed by dobey
Status: Merged
Merged at revision: not available
Proposed branch: lp:~ubuntuone-control-tower/ubuntu/lucid/ubuntuone-client/trunk
Merge into: lp:ubuntu/lucid/ubuntuone-client
Diff against target: 6279 lines (+3033/-1028)
57 files modified
Makefile.am (+1/-0)
Makefile.in (+1/-0)
bin/u1sdtool (+0/-1)
bin/ubuntuone-launch (+78/-0)
bin/ubuntuone-preferences (+796/-480)
bin/ubuntuone-syncdaemon (+9/-7)
configure (+10/-10)
configure.ac (+1/-1)
contrib/mocker.py (+4/-1)
contrib/pylint-wrapper (+2/-1)
contrib/testing/testcase.py (+24/-0)
data/Makefile.am (+6/-0)
data/Makefile.in (+50/-22)
data/oauth_registration.d/ubuntuone (+10/-0)
data/source_ubuntuone-client.py (+17/-6)
data/syncdaemon.conf (+16/-0)
data/ubuntuone-launch.desktop.in (+7/-0)
data/ubuntuone-preferences.desktop.in (+1/-1)
debian/changelog (+13/-0)
debian/control (+0/-1)
debian/source/format (+2/-0)
docs/states_manager.svg (+225/-133)
docs/syncdaemon_dbus_api.txt (+9/-0)
nautilus/contacts-view.c (+136/-47)
nautilus/contacts-view.h (+3/-0)
nautilus/u1-contacts-picker.c (+111/-12)
nautilus/u1-contacts-picker.h (+3/-0)
nautilus/ubuntuone-nautilus.c (+19/-2)
tests/syncdaemon/test_action_queue.py (+60/-1)
tests/syncdaemon/test_config.py (+39/-0)
tests/syncdaemon/test_dbus.py (+17/-0)
tests/syncdaemon/test_eq_inotify.py (+184/-35)
tests/syncdaemon/test_eventqueue.py (+36/-0)
tests/syncdaemon/test_fileshelf.py (+39/-3)
tests/syncdaemon/test_fsm.py (+304/-30)
tests/syncdaemon/test_hashqueue.py (+3/-2)
tests/syncdaemon/test_localrescan.py (+51/-1)
tests/syncdaemon/test_states.py (+8/-21)
tests/syncdaemon/test_u1sdtool.py (+13/-2)
tests/syncdaemon/test_vm.py (+187/-1)
tests/test_preferences.py (+135/-19)
ubuntuone/oauthdesktop/key_acls.py (+1/-1)
ubuntuone/oauthdesktop/main.py (+3/-4)
ubuntuone/syncdaemon/action_queue.py (+8/-8)
ubuntuone/syncdaemon/config.py (+9/-0)
ubuntuone/syncdaemon/dbus_interface.py (+42/-14)
ubuntuone/syncdaemon/event_queue.py (+77/-21)
ubuntuone/syncdaemon/file_shelf.py (+9/-0)
ubuntuone/syncdaemon/filesystem_manager.py (+50/-27)
ubuntuone/syncdaemon/hash_queue.py (+4/-3)
ubuntuone/syncdaemon/local_rescan.py (+82/-55)
ubuntuone/syncdaemon/logger.py (+9/-0)
ubuntuone/syncdaemon/main.py (+3/-2)
ubuntuone/syncdaemon/states.py (+6/-0)
ubuntuone/syncdaemon/sync.py (+6/-14)
ubuntuone/syncdaemon/tools.py (+36/-1)
ubuntuone/syncdaemon/volume_manager.py (+58/-38)
To merge this branch: bzr merge lp:~ubuntuone-control-tower/ubuntu/lucid/ubuntuone-client/trunk
Reviewer Review Type Date Requested Status
Ubuntu branches Pending
Review via email: mp+22582@code.launchpad.net
To post a comment you must log in.
24. By Luke Yelavich

* New upstream release.
  - Notify user when approaching and exceeding quota (LP: #540360)
  - Add instructional text to ubuntuone-preferences (LP: #539676)
  - UbuntuOne needs to autostart and connect by defualt (LP: #534707)
  - Impossible to infer status of file synchronization (LP: #526084)
  - Devices and Services tabs functionality and development (LP: #525803)
* Add debian/source/format.
* Remove python-httplib2 from the dependencies (LP: #535207)

25. By Luke Yelavich

releasing version 1.1.90-0ubuntu1

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile.am'
2--- Makefile.am 2010-02-17 23:51:29 +0000
3+++ Makefile.am 2010-03-31 23:55:45 +0000
4@@ -19,6 +19,7 @@
5 # Install our scripts and extra data here
6 bin_SCRIPTS = \
7 bin/ubuntuone-preferences \
8+ bin/ubuntuone-launch \
9 bin/u1sdtool \
10 bin/u1sync
11
12
13=== modified file 'Makefile.in'
14--- Makefile.in 2010-02-17 23:51:29 +0000
15+++ Makefile.in 2010-03-31 23:55:45 +0000
16@@ -296,6 +296,7 @@
17 # Install our scripts and extra data here
18 bin_SCRIPTS = \
19 bin/ubuntuone-preferences \
20+ bin/ubuntuone-launch \
21 bin/u1sdtool \
22 bin/u1sync
23
24
25=== modified file 'bin/u1sdtool'
26--- bin/u1sdtool 2010-03-10 23:36:53 +0000
27+++ bin/u1sdtool 2010-03-31 23:55:45 +0000
28@@ -31,7 +31,6 @@
29 from twisted.internet import reactor, defer
30
31 from ubuntuone.syncdaemon.tools import SyncDaemonTool
32-from ubuntuone.syncdaemon.dbus_interface import DBUS_IFACE_NAME
33 from ubuntuone.syncdaemon.tools import (
34 show_path_info,
35 show_uploads,
36
37=== added file 'bin/ubuntuone-launch'
38--- bin/ubuntuone-launch 1970-01-01 00:00:00 +0000
39+++ bin/ubuntuone-launch 2010-03-31 23:55:45 +0000
40@@ -0,0 +1,78 @@
41+#!/usr/bin/python
42+
43+# ubuntuone-start - Ubuntu One storage synchronization daemon startup helper
44+#
45+# Author: John Lenton <john.lenton@canonical.com>
46+#
47+# Copyright 2010 Canonical Ltd.
48+#
49+# This program is free software: you can redistribute it and/or modify it
50+# under the terms of the GNU General Public License version 3, as published
51+# by the Free Software Foundation.
52+#
53+# This program is distributed in the hope that it will be useful, but
54+# WITHOUT ANY WARRANTY; without even the implied warranties of
55+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
56+# PURPOSE. See the GNU General Public License for more details.
57+#
58+# You should have received a copy of the GNU General Public License along
59+# with this program. If not, see <http://www.gnu.org/licenses/>.
60+
61+import sys
62+import os
63+
64+U1ROOT=os.path.expanduser('~/Ubuntu One/')
65+
66+if __name__ == '__main__':
67+ # this check is done early to speed up startup on people who are not
68+ # (yet) using the service (this way avoids all the imports).
69+ if not os.path.isdir(U1ROOT):
70+ # no directory, just quit
71+ sys.exit(1)
72+
73+import dbus
74+import glib
75+import gobject
76+import gnomekeyring
77+from dbus.mainloop.glib import DBusGMainLoop
78+from ubuntuone.syncdaemon.tools import SyncDaemonTool, is_running
79+from twisted.internet import defer
80+
81+def stage_two(node, sync_daemon_tool):
82+ """do the last few checks and ask for a connect if all is ok"""
83+ d = None
84+ if node['node_id'] is not None:
85+ items = gnomekeyring.find_items_sync(
86+ gnomekeyring.ITEM_GENERIC_SECRET,
87+ {'ubuntuone-realm': 'https://ubuntuone.com'})
88+ if items:
89+ d = sync_daemon_tool.connect()
90+ if d is None:
91+ d = defer.fail(RuntimeError("bad node"))
92+ return d
93+
94+def main():
95+ """Start syncdaemon and ask it to connect, if possible."""
96+ gobject.set_application_name("ubuntuone-launch")
97+ loop = DBusGMainLoop(set_as_default=True)
98+ bus = dbus.SessionBus(mainloop=loop)
99+ sync_daemon_tool = SyncDaemonTool(bus)
100+
101+ if not is_running():
102+ try:
103+ d = sync_daemon_tool.start()
104+ except dbus.exception.DBusException, e:
105+ d = defer.fail(e)
106+ else:
107+ d = defer.succeed(True)
108+
109+ d.addCallback(lambda _: sync_daemon_tool.get_metadata(U1ROOT))
110+ d.addCallback(stage_two, sync_daemon_tool)
111+ # os._exit feels like it's cheating, but it's simple and fast
112+ d.addCallbacks(lambda _: os._exit(0), lambda _: os._exit(1))
113+
114+ mainloop = glib.MainLoop()
115+ mainloop.run()
116+
117+if __name__ == '__main__':
118+ main()
119
120=== modified file 'bin/ubuntuone-preferences'
121--- bin/ubuntuone-preferences 2010-03-10 23:36:53 +0000
122+++ bin/ubuntuone-preferences 2010-03-31 23:55:45 +0000
123@@ -1,4 +1,5 @@
124 #!/usr/bin/python
125+# -*- coding: utf-8 -*-
126
127 # ubuntuone-client-applet - Tray icon applet for managing Ubuntu One
128 #
129@@ -23,37 +24,47 @@
130 import pygtk
131 pygtk.require('2.0')
132 import gobject
133-import glib
134 import gtk
135 import os
136+import sys
137+import time
138 import gettext
139 import gnomekeyring
140-import httplib2
141 import simplejson
142+import subprocess
143+from threading import Thread
144 from oauth import oauth
145 from ubuntuone import clientdefs
146 from ubuntuone.syncdaemon.tools import SyncDaemonTool
147-from ubuntuone.syncdaemon.logger import LOGFOLDER
148+from ubuntuone.syncdaemon.logger import (basic_formatter, LOGFOLDER,
149+ CustomRotatingFileHandler)
150
151 import logging
152-import sys
153 import httplib, urlparse, socket
154
155 import dbus.service
156-from ConfigParser import ConfigParser
157 from dbus.exceptions import DBusException
158 from dbus.mainloop.glib import DBusGMainLoop
159-from xdg.BaseDirectory import xdg_config_home
160
161-logging.basicConfig(
162- filename=os.path.join(LOGFOLDER, 'u1-prefs.log'),
163- level=logging.DEBUG,
164- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
165 logger = logging.getLogger("ubuntuone-preferences")
166+logger.setLevel(logging.DEBUG)
167+handler = CustomRotatingFileHandler(filename=os.path.join(LOGFOLDER,
168+ 'u1-prefs.log'))
169+handler.setFormatter(basic_formatter)
170+handler.setLevel(logging.DEBUG)
171+logger.addHandler(handler)
172+
173+_ = gettext.gettext
174+
175+dcouch = None
176+# Try importing the Ubuntu One desktopcouch enablement api
177+try:
178+ from desktopcouch.replication_services import ubuntuone as dcouch
179+except ImportError:
180+ logger.error(_("DesktopCouch replication API not found"))
181+
182 DBusGMainLoop(set_as_default=True)
183
184-_ = gettext.gettext
185-
186 DBUS_IFACE_NAME = "com.ubuntuone.SyncDaemon"
187 DBUS_IFACE_CONFIG_NAME = DBUS_IFACE_NAME + ".Config"
188 DBUS_IFACE_STATUS_NAME = DBUS_IFACE_NAME + ".Status"
189@@ -61,6 +72,12 @@
190 DBUS_IFACE_AUTH_NAME = "com.ubuntuone.Authentication"
191 DBUS_IFACE_AUTH_PATH = "/"
192
193+# Our own DBus interface
194+PREFS_BUS_NAME = "com.ubuntuone.Preferences"
195+
196+# This is where thy music lies
197+U1MSPATH = os.path.expanduser("~/.ubuntuone/Purchased from Ubuntu One")
198+
199 # Why thank you GTK+ for enforcing style-set and breaking API
200 RCSTYLE = """
201 style 'dialogs' {
202@@ -71,19 +88,87 @@
203 widget_class '*Dialog*' style 'dialogs'
204 """
205
206-def dbus_async(*args):
207- """Simple handler to make dbus do stuff async."""
208- pass
209-
210+# Some defines for the bindwood installation magic
211+BW_PKG_NAME = "xul-ext-bindwood"
212+BW_INST_ARGS = ['apturl', 'apt:%s?section=universe' % BW_PKG_NAME]
213+BW_CHCK_ARGS = ['dpkg', '-l', BW_PKG_NAME]
214+
215+# This is a global so we can avoid creating multiple instances in some cases
216+prefs_dialog = None
217+
218+def dbus_async(*args, **kwargs):
219+ """Simple handler to make dbus do stuff async."""
220+ pass
221+
222+
223+def do_login_request(bus, error_handler):
224+ """Make a login request to the login handling daemon."""
225+ try:
226+ client = bus.get_object(DBUS_IFACE_AUTH_NAME,
227+ DBUS_IFACE_AUTH_PATH,
228+ follow_name_owner_changes=True)
229+ iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
230+ iface.login('https://ubuntuone.com', 'ubuntuone',
231+ reply_handler=dbus_async,
232+ error_handler=error_handler)
233+ except DBusException, e:
234+ error_handler(e)
235
236 def get_access_token(keyring):
237- items = []
238- items = keyring.find_items_sync(
239+ """Get the access token from the keyring."""
240+ items = []
241+ try:
242+ items = keyring.find_items_sync(
243 keyring.ITEM_GENERIC_SECRET,
244 {'ubuntuone-realm': "https://ubuntuone.com",
245 'oauth-consumer-key': 'ubuntuone'})
246- secret = items[0].secret
247- return oauth.OAuthToken.from_string(secret)
248+ secret = items[0].secret
249+ return oauth.OAuthToken.from_string(secret)
250+ except (gnomekeyring.NoMatchError, gnomekeyring.DeniedError):
251+ return None
252+
253+def do_rest_request(url, method, token, callback):
254+ """
255+ Helper that handles the REST response.
256+ """
257+ result = None
258+ consumer = oauth.OAuthConsumer('ubuntuone', 'hammertime')
259+
260+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(
261+ http_url=url,
262+ http_method=method,
263+ oauth_consumer=consumer,
264+ token=token,
265+ parameters='')
266+ oauth_request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
267+ consumer, token)
268+
269+ scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
270+
271+ conn = httplib.HTTPSConnection(netloc)
272+ try:
273+ conn.request(method, path, headers=oauth_request.to_header())
274+ response = conn.getresponse() # shouldn't block
275+ if response.status == 200:
276+ data = response.read() # neither should this
277+ result = simplejson.loads(data)
278+ else:
279+ result = {'status' : response.status,
280+ 'reason' : response.reason}
281+ except socket.error, e:
282+ result = {'error' : e}
283+
284+ gtk.gdk.threads_enter()
285+ callback(result)
286+ gtk.gdk.threads_leave()
287+
288+def make_rest_request(url=None, method='GET',
289+ callback=None, keyring=None):
290+ """
291+ Helper that makes an oauth-wrapped REST request.
292+ """
293+ token = get_access_token(keyring)
294+ Thread(target=do_rest_request, args=(url, method, token, callback)).start()
295
296
297 class DevicesWidget(gtk.Table):
298@@ -125,8 +210,8 @@
299 self.status_label = gtk.Label("")
300 self.attach(self.status_label, 0, 3, 2, 3)
301
302- self.description = gtk.Label(_("The devices connected to with your"
303- " personal cloud network"
304+ self.description = gtk.Label(_("The devices connected with your"
305+ " pseronal cloud network"
306 " are listed below"))
307 self.description.set_alignment(0., .5)
308 self.description.set_line_wrap(True)
309@@ -227,34 +312,6 @@
310 self.status_label.set_markup("<b>Error:</b> %s" % msg)
311 logger.error(msg)
312
313- def request(self, path='', method='GET'):
314- """
315- Helper that makes an oauth-wrapped rest request.
316-
317- XXX duplication with request_REST_info (but this one should be async).
318- """
319- url = self.base_url + path
320-
321- token = get_access_token(self.keyring)
322-
323- oauth_request = oauth.OAuthRequest.from_consumer_and_token(
324- http_url=url,
325- http_method=method,
326- oauth_consumer=self.consumer,
327- token=token,
328- parameters='')
329- oauth_request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
330- self.consumer, token)
331-
332- scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
333-
334- conn = httplib.HTTPSConnection(netloc)
335- try:
336- conn.request(method, path, headers=oauth_request.to_header())
337- except socket.error:
338- return None
339- return conn
340-
341 def get_devices(self):
342 """
343 Ask the server for a list of devices
344@@ -262,36 +319,38 @@
345 Hook up parse_devices to run on the result (when it gets here).
346 """
347 try:
348- token = get_access_token(self.keyring)
349+ get_access_token(self.keyring)
350 except gnomekeyring.NoMatchError:
351 self.error("No token in the keyring")
352- self.devices = []
353+ self.devices = None
354 else:
355- self.consumer = oauth.OAuthConsumer("ubuntuone", "hammertime")
356-
357- self.conn = self.request()
358- if self.conn is None:
359- self.clear_devices_view()
360- self.error('Unable to connect')
361- else:
362- glib.io_add_watch(
363- self.conn.sock,
364- glib.IO_IN | glib.IO_PRI | glib.IO_ERR | glib.IO_HUP,
365- self.parse_devices)
366-
367- def parse_devices(self, *a):
368+ make_rest_request(url=self.base_url, keyring=self.keyring,
369+ callback=self.parse_devices)
370+
371+ def parse_devices(self, result):
372 """
373 Parse the list of devices, and hook up list_devices if it worked.
374 """
375- response = self.conn.getresponse() # shouldn't block
376- if response.status == 200:
377- response = response.read() # neither should this
378- self.devices = simplejson.loads(response)
379- gobject.idle_add(self.list_devices)
380+ is_error = False
381+ error = None
382+ if result and isinstance(result, list):
383+ self.devices = result
384+ elif isinstance(result, dict):
385+ error = result.get('error', None)
386+ if not error and result.get('status', None):
387+ is_error = True
388+ error = result.get('reason', None)
389+ else:
390+ is_error = True
391 else:
392- self.clear_devices_view()
393- self.error(response.reason)
394- return False
395+ error = "Got empty result for devices list."
396+ is_error = True
397+
398+ if is_error:
399+ self.devices = None
400+ self.error(error)
401+
402+ gobject.idle_add(self.list_devices)
403
404 def clear_devices_view(self):
405 """
406@@ -315,8 +374,6 @@
407 If the list of devices is empty, make a fake one that refers
408 to the local machine (to get the connect/restart buttons).
409 """
410- self.resize(len(self.devices)+1, 3)
411-
412 self.clear_devices_view()
413
414 token = get_access_token(self.keyring)
415@@ -325,8 +382,10 @@
416 # a stopgap device so you can at least try to connect
417 self.devices = [{'kind': 'Computer',
418 'description': _("<LOCAL MACHINE>"),
419- 'token': token.key,
420+ 'token': token.key if token else '',
421 'FAKE': 'YES'}]
422+ else:
423+ self.resize(len(self.devices)+1, 3)
424
425 self.status_label.set_label("")
426
427@@ -340,17 +399,18 @@
428 self.attach(img, 0, 1, i, i+1)
429 self.attach(desc, 1, 2, i, i+1)
430 if 'FAKE' not in row:
431- # we don't include the "Remove" button for the fake entry :)
432- butn = gtk.Button(_('Remove'))
433- butn.connect('clicked', self.remove,
434- row['kind'], row.get('token'))
435- self.attach(butn, 2, 3, i, i+1, xoptions=0, yoptions=0)
436- if row.get('token') == token.key:
437+ # we don't include the "Remove" button for the fake entry :)
438+ butn = gtk.Button(_("Remove"))
439+ butn.connect('clicked', self.remove,
440+ row['kind'], row.get('token'))
441+ self.attach(butn, 2, 3, i, i+1, xoptions=0, yoptions=0)
442+ if token and row.get('token') == token.key or 'FAKE' in row:
443 self.bw_chk = ck_btn = gtk.CheckButton(
444- _("_Limit Bandwidth Usage"))
445+ _("_Limit Bandwidth Usage"))
446 ck_btn.set_active(self.bw_limited)
447 up_lbl = gtk.Label(_("Maximum _upload speed (KB/s):"))
448 up_lbl.set_alignment(0., .5)
449+ up_lbl.set_use_underline(True)
450 adj = gtk.Adjustment(value=self.up_limit/1024.,
451 lower=0.0, upper=4096.0,
452 step_incr=1.0, page_incr=16.0)
453@@ -359,6 +419,7 @@
454 up_lbl.set_mnemonic_widget(up_btn)
455 dn_lbl = gtk.Label(_("Maximum _download speed (KB/s):"))
456 dn_lbl.set_alignment(0., .5)
457+ dn_lbl.set_use_underline(True)
458 adj = gtk.Adjustment(value=self.dn_limit/1024.,
459 lower=0.0, upper=4096.0,
460 step_incr=1.0, page_incr=16.0)
461@@ -370,13 +431,13 @@
462 self.handle_bw_checkbox_toggled(ck_btn,
463 up_lbl, up_btn, dn_lbl, dn_btn)
464
465- self.conn_btn = gtk.Button(_('Connect'))
466+ self.conn_btn = gtk.Button(_("Connect"))
467 if self.connected is None:
468 self.conn_btn.set_sensitive(False)
469 elif self.connected:
470- self.conn_btn.set_label(_('Disconnect'))
471+ self.conn_btn.set_label(_("Disconnect"))
472 self.conn_btn.connect('clicked', self.handle_connect_button)
473- restart_btn = gtk.Button(_('Restart'))
474+ restart_btn = gtk.Button(_("Restart"))
475 restart_btn.connect('clicked', self.handle_restart_button)
476 btn_box = gtk.HButtonBox()
477 btn_box.add(self.conn_btn)
478@@ -401,9 +462,9 @@
479 """
480 self.conn_btn.set_sensitive(False)
481 if self.connected:
482- d = self.sdtool.disconnect()
483+ self.sdtool.disconnect()
484 else:
485- d = self.sdtool.connect()
486+ self.sdtool.connect()
487
488 def handle_restart_button(self, *a):
489 """
490@@ -417,402 +478,657 @@
491
492 Starts an async request to remove a device.
493 """
494- self.conn = self.request('remove/%s/%s' % (kind.lower(), token))
495- if self.conn is None:
496- self.clear_devices_view()
497- self.error('Unable to connect')
498- else:
499- glib.io_add_watch(
500- self.conn.sock,
501- glib.IO_IN | glib.IO_PRI | glib.IO_ERR | glib.IO_HUP,
502- self.parse_devices)
503+ make_rest_request(url=('%sremove/%s/%s' % (self.base_url,
504+ kind.lower(), token)),
505+ keyring=self.keyring,
506+ callback=self.parse_devices)
507+ local = get_access_token(self.keyring)
508+ def local_removal_cb(*args, **kwargs):
509+ """Try to get a new token if we remove the local one."""
510+ do_login_request(self.bus, self.error)
511
512+ if token == local.key:
513+ try:
514+ client = self.bus.get_object(DBUS_IFACE_AUTH_NAME,
515+ DBUS_IFACE_AUTH_PATH,
516+ follow_name_owner_changes=True)
517+ iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
518+ iface.clear_token('https://ubuntuone.com', 'ubuntuone',
519+ reply_handler=local_removal_cb,
520+ error_handler=self.error)
521+ except DBusException, e:
522+ self.error(e)
523
524
525 class UbuntuOneDialog(gtk.Dialog):
526- """Preferences dialog."""
527-
528- def __init__(self, config=None, keyring=gnomekeyring, *args, **kw):
529- """Initializes our config dialog."""
530- super(UbuntuOneDialog, self).__init__(*args, **kw)
531- self.set_title(_("Ubuntu One Preferences"))
532- self.set_has_separator(False)
533- self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
534- self.set_default_response(gtk.RESPONSE_CLOSE)
535- self.set_icon_name("ubuntuone")
536-
537- self.connect("close", self.__handle_response, gtk.RESPONSE_CLOSE)
538- self.connect("response", self.__handle_response)
539-
540- self.__bus = dbus.SessionBus()
541- self.keyring = keyring
542-
543- self.__bus.add_signal_receiver(
544- handler_function=self.__got_state,
545- signal_name='StatusChanged',
546- dbus_interface=DBUS_IFACE_STATUS_NAME)
547-
548- try:
549- client = self.__bus.get_object(DBUS_IFACE_NAME, "/config",
550- follow_name_owner_changes=True)
551- iface = dbus.Interface(client, DBUS_IFACE_CONFIG_NAME)
552- iface.get_throttling_limits(
553- reply_handler=self.__got_limits,
554- error_handler=self.__dbus_error)
555- iface.bandwidth_throttling_enabled(
556- reply_handler=self.__got_enabled,
557- error_handler=self.__dbus_error)
558- except DBusException, e:
559- self.__dbus_error(e)
560-
561- # Timeout ID to avoid spamming DBus from spinbutton changes
562- self.__update_id = 0
563-
564- # SD Tool object
565- self.sdtool = SyncDaemonTool(self.__bus)
566- self.sdtool.get_status().addCallbacks(lambda _: self.__got_state,
567- self.__sd_error)
568- # Build the dialog
569- self.__construct()
570- logger.debug("starting")
571-
572- def quit(self):
573- """Exit the main loop."""
574- gtk.main_quit()
575-
576- def __dbus_error(self, error):
577- """Error getting throttling config."""
578- print repr(error)
579-
580- def __sd_error(self, error):
581- """Error talking to syncdaemon."""
582- print repr(error)
583-
584- def __got_state(self, state):
585- """Got the state of syncdaemon."""
586- self.devices.handle_state_change(state)
587-
588- def __got_limits(self, limits):
589- """Got the throttling limits."""
590- logger.debug("got limits: %s" % (limits,))
591- self.devices.handle_limits(limits)
592-
593- def __got_enabled(self, enabled):
594- """Got the throttling enabled config."""
595- self.devices.handle_throttling_enabled(enabled)
596-
597- def __handle_response(self, dialog, response):
598- """Handle the dialog's response."""
599- self.hide()
600- self.devices.update_bw_settings()
601- gtk.main_quit()
602-
603- def _format_for_gb_display(self, bytes):
604- """Format bytes into reasonable gb display."""
605- gb = bytes / 1024 / 1024 / 1024
606- if gb < 1.0:
607- mb = bytes / 1024 / 1024
608- if mb < 1.0:
609- return (bytes / 1024, 'KB')
610- return (mb, 'MB')
611- return (gb, 'GB')
612-
613- def update_quota_display(self, used, total):
614- """Update the quota display."""
615- percent = (float(used) / float(total)) * 100
616- real_used, real_type = self._format_for_gb_display(used)
617- self.usage_label.set_text(
618- _("%(used)0.1f %(type)s Used (%(percent)0.1f%%)") % {
619- 'used' : real_used,
620- 'type' : real_type,
621- 'percent' : percent })
622- self.usage_graph.set_fraction(percent / 100)
623-
624- def request_REST_info(self, url, method):
625- """Make a REST request and return the resulting dict, or None."""
626- consumer = oauth.OAuthConsumer("ubuntuone", "hammertime")
627- token = get_access_token(self.keyring)
628- request = oauth.OAuthRequest.from_consumer_and_token(
629- http_url=url, http_method=method, oauth_consumer=consumer,
630- token=token)
631- request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
632- consumer, token)
633- client = httplib2.Http()
634- headers = {}
635- headers.update(request.to_header())
636- resp, content = client.request(url, method, headers=headers)
637- if resp['status'] == '200':
638- return simplejson.loads(content)
639- # FIXME: Log json parsing failures
640- else:
641- # FIXME: Log errors
642- return None
643-
644- def request_quota_info(self):
645- """Request new quota info from server, and update display."""
646- quota = self.request_REST_info('https://one.ubuntu.com/api/quota/',
647- 'GET')
648- if quota:
649- self.update_quota_display(quota['used'], quota['total'])
650-
651- def request_account_info(self):
652- """Request account info from server, and update display."""
653- user = self.request_REST_info('https://one.ubuntu.com/api/account/',
654- 'GET')
655- if user:
656- self.name_label.set_text(user['nickname'])
657- self.user_label.set_text(user['username'])
658- self.mail_label.set_text(user['email'])
659-
660- def toggle_db_sync(self, dbname, disable=False):
661- """
662- Toggle whether a db in desktopcouch is synchronized to the
663- Ubuntu One couchdb server.
664- """
665- # FIXME: Actually enable/disable the dbs if desktopcouch exists
666-
667- def __db_check_toggled(self, checkbutton):
668- """Handle toggling a desktopcouch service toggling."""
669- dbname = checkbutton.get_data('dbname')
670- self.toggle_db_sync(dbname, not checkbutton.get_active())
671-
672- def __files_check_toggled(self, checkbutton):
673- """Handle toggling the files service."""
674- enable = checkbutton.get_active()
675- if enable:
676- self.music_check.set_sensitive(True)
677- if self.music_check.get_active():
678- self.__music_check_toggled(self.music_check)
679- else:
680- self.music_check.set_sensitive(False)
681- # FIXME: Actually stop or start ubuntuone-syncdaemon
682-
683- def __music_check_toggled(self, checkbutton):
684- """Handle toggling the music download service."""
685- if not self.files_check.get_active():
686- return
687- # FIXME: Actually subscribe or unsubscribe to the UDF
688-
689- def __construct(self):
690- """Construct the dialog's layout."""
691- area = self.get_content_area()
692-
693- vbox = gtk.VBox(spacing=12)
694- vbox.set_border_width(12)
695- area.add(vbox)
696- vbox.show()
697-
698- # Usage text/progress bar
699- hbox = gtk.HBox(homogeneous=True)
700- vbox.pack_start(hbox, False, False)
701- hbox.show()
702-
703- label = gtk.Label("")
704- hbox.add(label)
705- label.show()
706-
707- rbox = gtk.VBox(spacing=2)
708- hbox.pack_end(rbox)
709- rbox.show()
710-
711- self.usage_label = gtk.Label("")
712- self.usage_label.set_alignment(0.5, 0.5)
713- rbox.add(self.usage_label)
714- self.usage_label.show()
715-
716- self.usage_graph = gtk.ProgressBar()
717- rbox.add(self.usage_graph)
718- self.usage_graph.show()
719-
720- self.update_quota_display(0, 2)
721-
722- # Notebook
723- self.notebook = gtk.Notebook()
724- vbox.add(self.notebook)
725- self.notebook.show()
726-
727- # Account tab
728- account = gtk.VBox(spacing=12)
729- account.set_border_width(6)
730- self.notebook.append_page(account)
731- self.notebook.set_tab_label_text(account, _("Account"))
732- account.show()
733-
734- # User info in account tab
735- table = gtk.Table(rows=3, columns=2)
736- table.set_row_spacings(6)
737- table.set_col_spacings(6)
738- account.pack_start(table, False, False)
739- table.show()
740-
741- label = gtk.Label(_("_Name:"))
742- label.set_use_underline(True)
743- label.set_alignment(0.0, 0.5)
744- table.attach(label, 0, 1, 0, 1)
745- label.show()
746-
747- self.name_label = gtk.Label("")
748- self.name_label.set_use_underline(True)
749- self.name_label.set_alignment(0.0, 0.5)
750- table.attach(self.name_label, 1, 2, 0, 1)
751- self.name_label.show()
752-
753- label = gtk.Label(_("_Username:"))
754- label.set_use_underline(True)
755- label.set_alignment(0.0, 0.5)
756- table.attach(label, 0, 1, 1, 2)
757- label.show()
758-
759- self.user_label = gtk.Label("")
760- self.user_label.set_use_underline(True)
761- self.user_label.set_alignment(0.0, 0.5)
762- table.attach(self.user_label, 1, 2, 1, 2)
763- self.user_label.show()
764-
765- label = gtk.Label(_("_E-mail:"))
766- label.set_use_underline(True)
767- label.set_alignment(0.0, 0.5)
768- table.attach(label, 0, 1, 2, 3)
769- label.show()
770-
771- self.mail_label = gtk.Label("")
772- self.mail_label.set_use_underline(True)
773- self.mail_label.set_alignment(0.0, 0.5)
774- table.attach(self.mail_label, 1, 2, 2, 3)
775- self.mail_label.show()
776-
777- # Devices tab
778- self.devices = DevicesWidget(self.__bus, self.keyring)
779- self.notebook.append_page(self.devices)
780- self.notebook.set_tab_label_text(self.devices, _("Devices"))
781- self.devices.show_all()
782- self.devices.get_devices()
783-
784- # Services tab
785- services = gtk.VBox(spacing=12)
786- services.set_border_width(6)
787- self.notebook.append_page(services)
788- self.notebook.set_tab_label_text(services, _("Services"))
789- # FIXME: These are all disabled for the moment
790- services.set_sensitive(False)
791- services.show()
792-
793- self.bookmarks_check = gtk.CheckButton(_("_Bookmarks"))
794- self.bookmarks_check.set_data('dbname', 'bookmarks')
795- self.bookmarks_check.connect('toggled', self.__db_check_toggled)
796- services.pack_start(self.bookmarks_check, False, False)
797- self.bookmarks_check.show()
798-
799- self.abook_check = gtk.CheckButton(_("C_ontacts"))
800- self.abook_check.set_data('dbname', 'contacts')
801- self.abook_check.connect('toggled', self.__db_check_toggled)
802- services.pack_start(self.abook_check, False, False)
803- self.abook_check.show()
804-
805- fbox = gtk.VBox(spacing=6)
806- services.pack_start(fbox, False, False)
807- fbox.show()
808-
809- self.files_check = gtk.CheckButton(_("_File Synchronization"))
810- self.files_check.set_active(True)
811- self.files_check.connect('toggled', self.__files_check_toggled)
812- fbox.pack_start(self.files_check, False, False)
813- self.files_check.show()
814-
815- alignment = gtk.Alignment(0.0, 0.5)
816- alignment.set_padding(0, 0, 12, 0)
817- fbox.pack_start(alignment, False, False)
818- alignment.show()
819-
820- self.music_check = gtk.CheckButton(_("_Music Download"))
821- self.music_check.set_active(True)
822- self.music_check.connect('toggled', self.__music_check_toggled)
823- alignment.add(self.music_check)
824- self.music_check.show()
825-
826-
827-class UbuntuoneLoginHandler(object):
828- """Class to handle registration/login, in case we aren't already."""
829-
830- def __init__(self, *args, **kw):
831- self.bus = dbus.SessionBus()
832-
833- self.newcreds = None
834- self.oautherr = None
835- self.authdeny = None
836-
837- def got_newcredentials(self, realm, consumer_key):
838- """Show our dialog, since we can do stuff now."""
839- self.disconnect_signal_handlers()
840- dialog = UbuntuOneDialog()
841- dialog.request_quota_info()
842- dialog.request_account_info()
843- dialog.show()
844-
845- def got_oautherror(self, message=None):
846- """Got an error during oauth."""
847- gtk.main_quit()
848-
849- def got_authdenied(self):
850- """User denied access."""
851- gtk.main_quit()
852-
853- def got_dbus_error(self, error):
854- """Got a DBusError."""
855- gtk.main_quit()
856-
857- def register_signal_handlers(self):
858- """Register the dbus signal handlers."""
859- self.newcreds = self.bus.add_signal_receiver(
860- handler_function=self.got_newcredentials,
861- signal_name='NewCredentials',
862- dbus_interface=DBUS_IFACE_AUTH_NAME)
863- self.oautherr = self.bus.add_signal_receiver(
864- handler_function=self.got_oautherror,
865- signal_name='OAuthError',
866- dbus_interface=DBUS_IFACE_AUTH_NAME)
867- self.authdeny = self.bus.add_signal_receiver(
868- handler_function=self.got_authdenied,
869- signal_name='AuthorizationDenied',
870- dbus_interface=DBUS_IFACE_AUTH_NAME)
871-
872- def disconnect_signal_handlers(self):
873- """Disconnect the dbus signal handlers, so we don't break."""
874- self.bus.remove_signal_receiver(
875- self.newcreds,
876- dbus_interface=DBUS_IFACE_AUTH_NAME)
877- self.bus.remove_signal_receiver(
878- self.oautherr,
879- dbus_interface=DBUS_IFACE_AUTH_NAME)
880- self.bus.remove_signal_receiver(
881- self.authdeny,
882- dbus_interface=DBUS_IFACE_AUTH_NAME)
883-
884- def do_login_check(self):
885- """Log in to U1 or validate that we already are."""
886- try:
887- client = self.bus.get_object(DBUS_IFACE_AUTH_NAME,
888- DBUS_IFACE_AUTH_PATH,
889- follow_name_owner_changes=True)
890- iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
891- iface.login('https://ubuntuone.com', 'ubuntuone',
892- reply_handler=dbus_async,
893- error_handler=self.got_dbus_error)
894- except DBusException, e:
895- self.got_dbus_error(e)
896+ """Preferences dialog."""
897+
898+ def __init__(self, config=None, keyring=gnomekeyring, *args, **kw):
899+ """Initializes our config dialog."""
900+ super(UbuntuOneDialog, self).__init__(*args, **kw)
901+ self.set_title(_("Ubuntu One Preferences"))
902+ self.set_has_separator(False)
903+ self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
904+ self.set_default_response(gtk.RESPONSE_CLOSE)
905+ self.set_icon_name("ubuntuone")
906+ self.set_default_size(400, 300)
907+
908+ self.connect("close", self.__handle_response, gtk.RESPONSE_CLOSE)
909+ self.connect("response", self.__handle_response)
910+
911+ # desktopcouch ReplicationExclusion, or None
912+ self.dcouch = None
913+
914+ # folder id for dealing with the Music folder
915+ self.ums_id = None
916+
917+ self.__bus = dbus.SessionBus()
918+ self.keyring = keyring
919+
920+ # Timeout ID to avoid spamming DBus from spinbutton changes
921+ self.__update_id = 0
922+
923+ # Build the dialog
924+ self.__construct()
925+
926+ # SD Tool object
927+ self.sdtool = SyncDaemonTool(self.__bus)
928+ self.sdtool.get_status().addCallbacks(self.__got_state,
929+ self.__sd_error)
930+
931+ # hook dbus up
932+ self.__bus.add_signal_receiver(
933+ handler_function=self.__got_state,
934+ signal_name='StatusChanged',
935+ dbus_interface=DBUS_IFACE_STATUS_NAME)
936+
937+ try:
938+ client = self.__bus.get_object(DBUS_IFACE_NAME, "/config",
939+ follow_name_owner_changes=True)
940+ iface = dbus.Interface(client, DBUS_IFACE_CONFIG_NAME)
941+ iface.get_throttling_limits(
942+ reply_handler=self.__got_limits,
943+ error_handler=self.__dbus_error)
944+ iface.bandwidth_throttling_enabled(
945+ reply_handler=self.__got_enabled,
946+ error_handler=self.__dbus_error)
947+ except DBusException, e:
948+ self.__dbus_error(e)
949+
950+ logger.debug("starting")
951+
952+ def __dbus_error(self, error):
953+ """Error getting throttling config."""
954+ logger.error(error)
955+
956+ def __sd_error(self, error):
957+ """Error talking to syncdaemon."""
958+ logger.error(error)
959+
960+ def __got_state(self, state):
961+ """Got the state of syncdaemon."""
962+ if not state['is_connected']:
963+ self.status_label.set_text(_("Disconnected"))
964+ else:
965+ try:
966+ status = state['name']
967+ queues = state['queues']
968+ except KeyError:
969+ status = None
970+ queues = None
971+ if status == u'QUEUE_MANAGER' and queues == u'IDLE':
972+ self.status_label.set_text(_("Synchronization complete"))
973+ else:
974+ self.status_label.set_text(_(u"Synchronization in progress…"))
975+
976+ # Update the stuff in the devices tab
977+ self.devices.handle_state_change(state)
978+
979+ def __got_limits(self, limits):
980+ """Got the throttling limits."""
981+ logger.debug("got limits: %s" % (limits,))
982+ self.devices.handle_limits(limits)
983+
984+ def __got_enabled(self, enabled):
985+ """Got the throttling enabled config."""
986+ self.devices.handle_throttling_enabled(enabled)
987+
988+ def __handle_response(self, dialog, response):
989+ """Handle the dialog's response."""
990+ self.hide()
991+ self.devices.handle_bw_controls_changed()
992+ gtk.main_quit()
993+
994+ def _format_for_gb_display(self, bytes):
995+ """Format bytes into reasonable gb display."""
996+ gb = bytes / 1024 / 1024 / 1024
997+ if gb < 1.0:
998+ mb = bytes / 1024 / 1024
999+ if mb < 1.0:
1000+ return (bytes / 1024, 'KB')
1001+ return (mb, 'MB')
1002+ return (gb, 'GB')
1003+
1004+ def update_quota_display(self, used, total):
1005+ """Update the quota display."""
1006+ percent = (float(used) / float(total)) * 100
1007+ real_used, real_type = self._format_for_gb_display(used)
1008+ self.usage_label.set_text(
1009+ _("%(used)0.1f %(type)s Used (%(percent)0.1f%%)") % {
1010+ 'used' : real_used,
1011+ 'type' : real_type,
1012+ 'percent' : percent })
1013+ self.usage_graph.set_fraction(percent / 100)
1014+
1015+ # assumes all paid accounts will have more than 50GB
1016+ is_paid_account = total >= 50<<30
1017+ self.plan_label.set_label(_("Paid") if is_paid_account else _("Free"))
1018+ if is_paid_account:
1019+ self.upgrade_link.hide()
1020+ else:
1021+ self.upgrade_link.show()
1022+
1023+ if percent >= 100.:
1024+ self.overquota_img.set_from_icon_name('dialog-warning',
1025+ gtk.ICON_SIZE_DIALOG)
1026+ self.overquota.show_all()
1027+ label = _("You are using all of your Ubuntu One storage.")
1028+ if is_paid_account:
1029+ desc = _("Synchronization is now disabled. Remove files from"
1030+ " synchronization or upgrade your subscription. Use"
1031+ " the Support options if an upgrade is not available.")
1032+ else:
1033+ desc = _("Synchronization is now disabled. Remove files from"
1034+ " synchronization or upgrade your subscription.")
1035+ elif percent >= 95.:
1036+ self.overquota_img.set_from_icon_name('dialog-information',
1037+ gtk.ICON_SIZE_DIALOG)
1038+ self.overquota.show_all()
1039+ label = _("You are near your Ubuntu One storage limit.")
1040+ if is_paid_account:
1041+ desc = _("Automatic synchronization will stop when you reach"
1042+ " your storage limit.")
1043+ else:
1044+ desc = _("Automatic synchronization will stop when you reach"
1045+ " your storage limit. Please consider upgrading to"
1046+ " avoid a service interruption.")
1047+ else:
1048+ self.overquota_img.clear()
1049+ label = desc = ""
1050+ self.overquota.hide_all()
1051+ self.overquota_label.set_markup("%s\n<small>%s</small>" % (label, desc))
1052+
1053+ def got_quota_info(self, quota):
1054+ """Handle the result from the quota REST call."""
1055+ if quota:
1056+ used = quota.get('used', 0)
1057+ total = quota.get('total', 2)
1058+ self.update_quota_display(used, total)
1059+
1060+ def request_quota_info(self):
1061+ """Request new quota info from server, and update display."""
1062+ make_rest_request(url='https://one.ubuntu.com/api/quota/',
1063+ keyring=self.keyring,
1064+ callback=self.got_quota_info)
1065+
1066+ def got_account_info(self, user):
1067+ """Handle the result from the account REST call."""
1068+ if user:
1069+ self.name_label.set_text(user.get('nickname', _("Unknown")))
1070+ self.mail_label.set_text(user.get('email', _("Unknown")))
1071+
1072+ def request_account_info(self):
1073+ """Request account info from server, and update display."""
1074+ make_rest_request(url='https://one.ubuntu.com/api/account/',
1075+ keyring=self.keyring,
1076+ callback=self.got_account_info)
1077+
1078+ def __bw_inst_cb(self, button):
1079+ """Pops off the bindwood installer to a thread."""
1080+ Thread(target=self.__install_bindwood).start()
1081+
1082+ def __install_bindwood(self):
1083+ """Runs the tool to intall bindwood and updates the UI."""
1084+ installed = subprocess.call(BW_INST_ARGS)
1085+ gtk.gdk.threads_enter()
1086+ if installed == 0:
1087+ self.bw_inst_box.hide()
1088+ else:
1089+ self.bw_inst_box.show()
1090+ logger.error(_("There was an error installing bindwood."))
1091+ gtk.gdk.threads_leave()
1092+
1093+ def __check_for_bindwood(self):
1094+ """
1095+ Method run in a thread to check that bindwood exists, and
1096+ update the interface appropriately.
1097+ """
1098+ exists = True
1099+ p = subprocess.Popen(BW_CHCK_ARGS, bufsize=4096,
1100+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1101+ stdin=subprocess.PIPE, env=os.environ)
1102+ result = p.wait()
1103+ if result == 0:
1104+ for line in p.stdout.readlines():
1105+ if line.startswith('un'):
1106+ exists = False
1107+ else:
1108+ exists = False
1109+
1110+ gtk.gdk.threads_enter()
1111+ if exists:
1112+ self.bw_inst_box.hide()
1113+ else:
1114+ self.bw_inst_btn.connect("clicked",
1115+ self.__bw_inst_cb)
1116+ self.bw_inst_box.show()
1117+ gtk.gdk.threads_leave()
1118+
1119+ def connect_desktopcouch_exclusion(self):
1120+ """Hook up to desktopcouch enablement API, or disable the UI."""
1121+ # Hook up to desktopcouch, or die trying
1122+ if dcouch:
1123+ Thread(target=self.__check_for_bindwood).start()
1124+
1125+ self.dcouch = dcouch.ReplicationExclusion()
1126+ exclusions = self.dcouch.all_exclusions()
1127+ for check in [self.bookmarks_check,
1128+ self.abook_check,
1129+ self.gwib_check]:
1130+ if check.get_data('dbname') in exclusions:
1131+ check.set_active(False)
1132+ else:
1133+ check.set_active(True)
1134+ else:
1135+ self.bookmarks_check.set_sensitive(False)
1136+ self.abook_check.set_sensitive(False)
1137+ self.gwib_check.set_sensitive(False)
1138+
1139+ def toggle_db_sync(self, dbname, disable=False):
1140+ """
1141+ Toggle whether a db in desktopcouch is synchronized to the
1142+ Ubuntu One couchdb server.
1143+ """
1144+ if self.dcouch:
1145+ if disable:
1146+ self.dcouch.exclude(dbname)
1147+ else:
1148+ self.dcouch.replicate(dbname)
1149+ else:
1150+ logger.error(_("Database enablement is unavailable."))
1151+
1152+ def __db_check_toggled(self, checkbutton):
1153+ """Handle toggling a desktopcouch service toggling."""
1154+ dbname = checkbutton.get_data('dbname')
1155+ self.toggle_db_sync(dbname, not checkbutton.get_active())
1156+
1157+ def files_check_toggled(self, checkbutton):
1158+ """Handle toggling the files service."""
1159+ enabled = checkbutton.get_active()
1160+ self.sdtool.enable_files_sync(enabled).addCallbacks(
1161+ lambda _: dbus_async,
1162+ self.__sd_error)
1163+ def sd_error(error):
1164+ self.files_check.set_active(False)
1165+ self.files_check.set_sensitive(False)
1166+ self.music_check.set_sensitive(False)
1167+ self.__sd_error(error)
1168+
1169+ if enabled:
1170+ self.sdtool.start().addErrback(sd_error)
1171+ if self.ums_id:
1172+ self.music_check.set_sensitive(True)
1173+ if self.music_check.get_active():
1174+ self.__music_check_toggled(self.music_check)
1175+ else:
1176+ self.sdtool.quit()
1177+ self.music_check.set_sensitive(False)
1178+
1179+ def music_check_toggled(self, checkbutton):
1180+ """Handle toggling the music download service."""
1181+ if not self.files_check.get_active() or not self.ums_id:
1182+ return
1183+ enabled = checkbutton.get_active()
1184+ def got_error(error):
1185+ checkbutton.set_sensitive(False)
1186+ checkbutton.set_active(not enabled)
1187+ self.__sd_error(error)
1188+
1189+ if enabled:
1190+ d = self.sdtool.subscribe_folder(self.ums_id)
1191+ else:
1192+ d = self.sdtool.unsubscribe_folder(self.ums_id)
1193+ d.addErrback(got_error)
1194+
1195+ def get_syncdaemon_sync_config(self):
1196+ """Update the files/music sync config from syncdaemon."""
1197+ def got_fs_err(error):
1198+ self.files_check.set_sensitive(False)
1199+ self.music_check.set_sensitive(False)
1200+ self.__sd_error(error)
1201+
1202+ def got_ms_err(error):
1203+ self.music_check.set_sensitive(False)
1204+ self.__sd_error(error)
1205+
1206+ def got_info(info):
1207+ if not info:
1208+ self.music_check.set_active(False)
1209+ self.music_check.set_sensitive(False)
1210+ else:
1211+ self.ums_id = info['volume_id']
1212+ self.music_check.set_sensitive(True)
1213+ if info['subscribed'] == 'True':
1214+ self.music_check.set_active(True)
1215+ else:
1216+ self.music_check.set_active(False)
1217+
1218+ def got_fs(enabled):
1219+ self.files_check.set_active(enabled)
1220+
1221+ self.sdtool.get_folder_info(U1MSPATH).addCallbacks(got_info, got_ms_err)
1222+ self.sdtool.is_files_sync_enabled().addCallbacks(got_fs, got_fs_err)
1223+
1224+ def connect_file_sync_callbacks(self):
1225+ """Connect the file sync checkbox callbacks."""
1226+ self.files_check.connect('toggled', self.files_check_toggled)
1227+ self.music_check.connect('toggled', self.music_check_toggled)
1228+
1229+ def __construct(self):
1230+ """Construct the dialog's layout."""
1231+ area = self.get_content_area()
1232+
1233+ vbox = gtk.VBox(spacing=12)
1234+ vbox.set_border_width(12)
1235+ area.add(vbox)
1236+ vbox.show()
1237+
1238+ # Usage text/progress bar
1239+ rbox = gtk.VBox(spacing=12)
1240+ vbox.pack_start(rbox, False, False)
1241+ rbox.show()
1242+
1243+ hbox = gtk.HBox(spacing=6)
1244+ rbox.pack_start(hbox, False, False)
1245+ hbox.show()
1246+
1247+ self.usage_graph = gtk.ProgressBar()
1248+ hbox.pack_start(self.usage_graph, False, False)
1249+ self.usage_graph.show()
1250+
1251+ self.usage_label = gtk.Label(_("Unknown"))
1252+ self.usage_label.set_alignment(0.0, 0.5)
1253+ hbox.pack_start(self.usage_label, False, False)
1254+ self.usage_label.show()
1255+
1256+ self.status_label = gtk.Label(_("Unknown"))
1257+ self.status_label.set_alignment(0.0, 0.5)
1258+ rbox.pack_start(self.status_label, False, False)
1259+ self.status_label.show()
1260+
1261+ # Notebook
1262+ self.notebook = gtk.Notebook()
1263+ vbox.add(self.notebook)
1264+ self.notebook.show()
1265+
1266+ # Account tab
1267+ account = gtk.VBox(spacing=12)
1268+ account.set_border_width(6)
1269+ self.notebook.append_page(account)
1270+ self.notebook.set_tab_label_text(account, _("Account"))
1271+ account.show()
1272+
1273+ # overquota notice in account tab
1274+ self.overquota = gtk.HBox(spacing=6)
1275+ self.overquota.set_border_width(6)
1276+ account.pack_start(self.overquota, False, False)
1277+
1278+ self.overquota_img = gtk.Image()
1279+ self.overquota.pack_start(self.overquota_img, False, False)
1280+ self.overquota_img.show()
1281+
1282+ self.overquota_label = label = gtk.Label("\n\n")
1283+ label.set_line_wrap(True)
1284+ label.set_alignment(0.0, 0.5)
1285+ self.overquota.pack_start(label, False, False)
1286+ self.overquota_label.show()
1287+
1288+
1289+ # User info in account tab
1290+ table = gtk.Table(rows=6, columns=2)
1291+ table.set_row_spacings(6)
1292+ table.set_col_spacings(6)
1293+ account.pack_start(table, False, False)
1294+ table.show()
1295+
1296+ label = gtk.Label(_("_Name:"))
1297+ label.set_use_underline(True)
1298+ label.set_alignment(0.0, 0.5)
1299+ table.attach(label, 0, 1, 0, 1)
1300+ label.show()
1301+
1302+ self.name_label = gtk.Label(_("Unknown"))
1303+ self.name_label.set_use_underline(True)
1304+ self.name_label.set_alignment(0.0, 0.5)
1305+ table.attach(self.name_label, 1, 2, 0, 1)
1306+ self.name_label.show()
1307+
1308+ label = gtk.Label(_("_E-mail:"))
1309+ label.set_use_underline(True)
1310+ label.set_alignment(0.0, 0.5)
1311+ table.attach(label, 0, 1, 1, 2)
1312+ label.show()
1313+
1314+ self.mail_label = gtk.Label(_("Unknown"))
1315+ self.mail_label.set_use_underline(True)
1316+ self.mail_label.set_alignment(0.0, 0.5)
1317+ table.attach(self.mail_label, 1, 2, 1, 2)
1318+ self.mail_label.show()
1319+
1320+ label = gtk.Label(_("Current plan:"))
1321+ label.set_alignment(0.0, 0.5)
1322+ table.attach(label, 0, 1, 2, 3)
1323+ label.show()
1324+
1325+ self.plan_label = gtk.Label(_("Unknown"))
1326+ self.plan_label.set_alignment(0.0, 0.5)
1327+ table.attach(self.plan_label, 1, 2, 2, 3)
1328+ self.plan_label.show()
1329+
1330+ for n, (url, label) in enumerate([
1331+ ("http://one.ubuntu.com/support", _("Support options")),
1332+ ("http://one.ubuntu.com/account", _("Manage account")),
1333+ ("http://one.ubuntu.com/upgrade",
1334+ _("Upgrade your subscription")),
1335+ ]):
1336+ link = gtk.LinkButton(url, label)
1337+ link.set_relief(gtk.RELIEF_NONE)
1338+ link.set_alignment(0.0, 0.5)
1339+ table.attach(link, 0, 2, 5-n, 6-n)
1340+ link.show()
1341+ self.upgrade_link = link
1342+ self.upgrade_link.hide()
1343+
1344+ # Devices tab
1345+ sw = gtk.ScrolledWindow()
1346+ sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1347+ self.notebook.append_page(sw)
1348+ self.notebook.set_tab_label_text(sw, _("Devices"))
1349+ sw.show()
1350+ self.devices = DevicesWidget(self.__bus, self.keyring)
1351+ sw.add_with_viewport(self.devices)
1352+ self.devices.show_all()
1353+
1354+ # Services tab
1355+ services = gtk.VBox(spacing=12)
1356+ services.set_border_width(6)
1357+ self.notebook.append_page(services)
1358+ self.notebook.set_tab_label_text(services, _("Services"))
1359+ services.show()
1360+
1361+ label = gtk.Label()
1362+ label.set_markup(
1363+ _("Ubuntu One sync options\n"
1364+ "<small>Choose services to synchronize"
1365+ " with this computer</small>"))
1366+ label.set_alignment(0., .5)
1367+ label.set_padding(12, 6)
1368+ label.set_line_wrap(True)
1369+ services.pack_start(label, False, False)
1370+ label.show()
1371+
1372+ self.bookmarks_check = gtk.CheckButton(_("_Bookmarks"))
1373+ self.bookmarks_check.set_data('dbname', 'bookmarks')
1374+ self.bookmarks_check.connect('toggled', self.__db_check_toggled)
1375+ services.pack_start(self.bookmarks_check, False, False)
1376+ self.bookmarks_check.show()
1377+
1378+ # This box is shown in the event bindwood is not installed
1379+ self.bw_inst_box = gtk.HBox(spacing=12)
1380+ services.pack_start(self.bw_inst_box, False, False)
1381+
1382+ label = gtk.Label("")
1383+ self.bw_inst_box.pack_start(label, False, False)
1384+ label.show()
1385+
1386+ label = gtk.Label("<i>%s</i>" % _("Firefox extension not installed."))
1387+ label.set_use_markup(True)
1388+ label.set_alignment(0.0, 0.5)
1389+ self.bw_inst_box.pack_start(label, False, False)
1390+ label.show()
1391+
1392+ self.bw_inst_btn = gtk.Button(_("_Install"))
1393+ self.bw_inst_box.pack_start(self.bw_inst_btn, False, False)
1394+ self.bw_inst_btn.show()
1395+
1396+ self.gwib_check = gtk.CheckButton(_("Broadcast Messages _Archive"))
1397+ self.gwib_check.set_data('dbname', 'gwibber_messages')
1398+ self.gwib_check.connect('toggled', self.__db_check_toggled)
1399+ services.pack_start(self.gwib_check, False, False)
1400+ self.gwib_check.show()
1401+
1402+ self.abook_check = gtk.CheckButton(_("C_ontacts"))
1403+ self.abook_check.set_data('dbname', 'contacts')
1404+ self.abook_check.connect('toggled', self.__db_check_toggled)
1405+ services.pack_start(self.abook_check, False, False)
1406+ self.abook_check.show()
1407+
1408+ fbox = gtk.VBox(spacing=6)
1409+ services.pack_start(fbox, False, False)
1410+ fbox.show()
1411+
1412+ self.files_check = gtk.CheckButton(_("_File Synchronization"))
1413+ self.files_check.set_active(True)
1414+ fbox.pack_start(self.files_check, False, False)
1415+ self.files_check.show()
1416+
1417+ alignment = gtk.Alignment(0.0, 0.5)
1418+ alignment.set_padding(0, 0, 12, 0)
1419+ fbox.pack_start(alignment, False, False)
1420+ alignment.show()
1421+
1422+ self.music_check = gtk.CheckButton(_("_Music Download"))
1423+ self.music_check.set_active(True)
1424+ alignment.add(self.music_check)
1425+ self.music_check.show()
1426+
1427+
1428+class UbuntuoneLoginHandler(dbus.service.Object):
1429+ """Class to handle registration/login, in case we aren't already."""
1430+
1431+ def __init__(self, dialog, *args, **kw):
1432+ self.bus = dbus.SessionBus()
1433+
1434+ # The actual UI
1435+ self.dialog = dialog
1436+
1437+ # DBus object magic
1438+ self.path = '/'
1439+ bus_name = dbus.service.BusName(PREFS_BUS_NAME, bus=self.bus)
1440+ dbus.service.Object.__init__(self, bus_name=bus_name,
1441+ object_path=self.path)
1442+
1443+
1444+ @dbus.service.method(PREFS_BUS_NAME, in_signature='', out_signature='')
1445+ def present(self):
1446+ """Raise the dialog window."""
1447+ self.dialog.connect_desktopcouch_exclusion()
1448+ self.dialog.get_syncdaemon_sync_config()
1449+ self.dialog.connect_file_sync_callbacks()
1450+ self.dialog.request_quota_info()
1451+ self.dialog.request_account_info()
1452+ self.dialog.devices.get_devices()
1453+ self.dialog.present_with_time(int(time.time()))
1454+
1455+ def got_newcredentials(self, realm, consumer_key):
1456+ """Show our dialog, since we can do stuff now."""
1457+ self.present()
1458+
1459+ def got_oautherror(self, message=None):
1460+ """Got an error during oauth."""
1461+ if message:
1462+ logger.error(message)
1463+ else:
1464+ logger.error(_("OAuthError with no message."))
1465+
1466+ def got_authdenied(self):
1467+ """User denied access."""
1468+ logger.error(_("Authorization was denied."))
1469+
1470+ def got_dbus_error(self, error):
1471+ """Got a DBusError."""
1472+ logger.error(error)
1473+
1474+ def register_signal_handlers(self):
1475+ """Register the dbus signal handlers."""
1476+ self.bus.add_signal_receiver(
1477+ handler_function=self.got_newcredentials,
1478+ signal_name='NewCredentials',
1479+ dbus_interface=DBUS_IFACE_AUTH_NAME)
1480+ self.bus.add_signal_receiver(
1481+ handler_function=self.got_oautherror,
1482+ signal_name='OAuthError',
1483+ dbus_interface=DBUS_IFACE_AUTH_NAME)
1484+ self.bus.add_signal_receiver(
1485+ handler_function=self.got_authdenied,
1486+ signal_name='AuthorizationDenied',
1487+ dbus_interface=DBUS_IFACE_AUTH_NAME)
1488
1489
1490 if __name__ == "__main__":
1491- gettext.bindtextdomain(clientdefs.GETTEXT_PACKAGE, clientdefs.LOCALEDIR)
1492- gettext.textdomain(clientdefs.GETTEXT_PACKAGE)
1493-
1494- gtk.rc_parse_string(RCSTYLE)
1495- gobject.set_application_name("Ubuntu One")
1496-
1497- try:
1498- login = UbuntuoneLoginHandler()
1499- login.register_signal_handlers()
1500- login.do_login_check()
1501- gtk.main()
1502- except KeyboardInterrupt:
1503- pass
1504+ gettext.bindtextdomain(clientdefs.GETTEXT_PACKAGE, clientdefs.LOCALEDIR)
1505+ gettext.textdomain(clientdefs.GETTEXT_PACKAGE)
1506+
1507+ gobject.threads_init()
1508+ gtk.gdk.threads_init()
1509+
1510+ gtk.rc_parse_string(RCSTYLE)
1511+ gobject.set_application_name("ubuntuone-preferences")
1512+
1513+ bus = dbus.SessionBus()
1514+ result = bus.request_name(PREFS_BUS_NAME,
1515+ dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
1516+ if result == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
1517+ try:
1518+ client = bus.get_object(PREFS_BUS_NAME, '/',
1519+ follow_name_owner_changes=True)
1520+ iface= dbus.Interface(client, PREFS_BUS_NAME)
1521+ iface.present()
1522+ except DBusException, e:
1523+ logger.error(e)
1524+ sys.exit(0)
1525+
1526+ try:
1527+ # The prefs dialog
1528+ gtk.gdk.threads_enter()
1529+ prefs_dialog = UbuntuOneDialog()
1530+ prefs_dialog.show()
1531+
1532+ login = UbuntuoneLoginHandler(dialog=prefs_dialog)
1533+ login.register_signal_handlers()
1534+ gobject.timeout_add_seconds(1, do_login_request,
1535+ bus, login.got_dbus_error)
1536+ gtk.main()
1537+ gtk.gdk.threads_leave()
1538+ except KeyboardInterrupt:
1539+ pass
1540
1541=== modified file 'bin/ubuntuone-syncdaemon'
1542--- bin/ubuntuone-syncdaemon 2010-03-04 16:47:43 +0000
1543+++ bin/ubuntuone-syncdaemon 2010-03-31 23:55:45 +0000
1544@@ -25,7 +25,6 @@
1545 import dbus
1546 import dbus.mainloop.glib
1547 import gobject
1548-import logging
1549 import os
1550 import signal
1551 import sys
1552@@ -34,15 +33,10 @@
1553 from ubuntuone.syncdaemon import dbus_interface, logger, config
1554 from ubuntuone.syncdaemon.config import (
1555 get_config_files,
1556- get_parsers,
1557- xdg_cache_dir_parser,
1558- xdg_data_dir_parser,
1559- home_dir_parser,
1560 )
1561
1562 from ubuntuone.syncdaemon.main import Main
1563
1564-from configglue import configglue
1565 from twisted.internet import reactor
1566 from xdg.BaseDirectory import (
1567 xdg_cache_home,
1568@@ -95,6 +89,10 @@
1569 if is_already_running():
1570 die('Another instance is running')
1571
1572+ # check if the user disabled files sync
1573+ if not config.get_user_config().get_files_sync_enabled():
1574+ die('Files synchronization is disabled.')
1575+
1576 # check if we are using xdg_data_home and it doesn't exists
1577 if xdg_data_home in options.data_dir and \
1578 not os.path.exists(options.data_dir):
1579@@ -132,7 +130,8 @@
1580 shares_symlink_name='Shared With Me',
1581 read_limit=options.bandwidth_throttling_read_limit,
1582 write_limit=options.bandwidth_throttling_write_limit,
1583- throttling_enabled=options.bandwidth_throttling_on)
1584+ throttling_enabled=options.bandwidth_throttling_on,
1585+ ignore_files=options.ignore)
1586 if options.oauth:
1587 try:
1588 (key, secret) = options.oauth.split(':', 2)
1589@@ -164,6 +163,9 @@
1590 except ImportError:
1591 logger.root_logger.warning('guppy-pe/heapy not available, remote '
1592 'monitor thread not started')
1593+ else:
1594+ guppy.heapy.RM.on()
1595+
1596 main.start()
1597 if options.debug_lsprof_file:
1598 try:
1599
1600=== modified file 'configure'
1601--- configure 2010-03-10 23:36:53 +0000
1602+++ configure 2010-03-31 23:55:45 +0000
1603@@ -1,6 +1,6 @@
1604 #! /bin/sh
1605 # Guess values for system-dependent variables and create Makefiles.
1606-# Generated by GNU Autoconf 2.65 for ubuntuone-client 1.1.4.
1607+# Generated by GNU Autoconf 2.65 for ubuntuone-client 1.1.90.
1608 #
1609 #
1610 # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1998, 1999, 2000, 2001,
1611@@ -698,8 +698,8 @@
1612 # Identity of this package.
1613 PACKAGE_NAME='ubuntuone-client'
1614 PACKAGE_TARNAME='ubuntuone-client'
1615-PACKAGE_VERSION='1.1.4'
1616-PACKAGE_STRING='ubuntuone-client 1.1.4'
1617+PACKAGE_VERSION='1.1.90'
1618+PACKAGE_STRING='ubuntuone-client 1.1.90'
1619 PACKAGE_BUGREPORT=''
1620 PACKAGE_URL=''
1621
1622@@ -1476,7 +1476,7 @@
1623 # Omit some internal or obsolete options to make the list less imposing.
1624 # This message is too long to be a string in the A/UX 3.1 sh.
1625 cat <<_ACEOF
1626-\`configure' configures ubuntuone-client 1.1.4 to adapt to many kinds of systems.
1627+\`configure' configures ubuntuone-client 1.1.90 to adapt to many kinds of systems.
1628
1629 Usage: $0 [OPTION]... [VAR=VALUE]...
1630
1631@@ -1547,7 +1547,7 @@
1632
1633 if test -n "$ac_init_help"; then
1634 case $ac_init_help in
1635- short | recursive ) echo "Configuration of ubuntuone-client 1.1.4:";;
1636+ short | recursive ) echo "Configuration of ubuntuone-client 1.1.90:";;
1637 esac
1638 cat <<\_ACEOF
1639
1640@@ -1660,7 +1660,7 @@
1641 test -n "$ac_init_help" && exit $ac_status
1642 if $ac_init_version; then
1643 cat <<\_ACEOF
1644-ubuntuone-client configure 1.1.4
1645+ubuntuone-client configure 1.1.90
1646 generated by GNU Autoconf 2.65
1647
1648 Copyright (C) 2009 Free Software Foundation, Inc.
1649@@ -1938,7 +1938,7 @@
1650 This file contains any messages produced by compilers while
1651 running configure, to aid debugging if configure makes a mistake.
1652
1653-It was created by ubuntuone-client $as_me 1.1.4, which was
1654+It was created by ubuntuone-client $as_me 1.1.90, which was
1655 generated by GNU Autoconf 2.65. Invocation command line was
1656
1657 $ $0 $@
1658@@ -2748,7 +2748,7 @@
1659
1660 # Define the identity of the package.
1661 PACKAGE='ubuntuone-client'
1662- VERSION='1.1.4'
1663+ VERSION='1.1.90'
1664
1665
1666 cat >>confdefs.h <<_ACEOF
1667@@ -12972,7 +12972,7 @@
1668 # report actual input values of CONFIG_FILES etc. instead of their
1669 # values after options handling.
1670 ac_log="
1671-This file was extended by ubuntuone-client $as_me 1.1.4, which was
1672+This file was extended by ubuntuone-client $as_me 1.1.90, which was
1673 generated by GNU Autoconf 2.65. Invocation command line was
1674
1675 CONFIG_FILES = $CONFIG_FILES
1676@@ -13038,7 +13038,7 @@
1677 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
1678 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
1679 ac_cs_version="\\
1680-ubuntuone-client config.status 1.1.4
1681+ubuntuone-client config.status 1.1.90
1682 configured by $0, generated by GNU Autoconf 2.65,
1683 with options \\"\$ac_cs_config\\"
1684
1685
1686=== modified file 'configure.ac'
1687--- configure.ac 2010-03-10 23:36:53 +0000
1688+++ configure.ac 2010-03-31 23:55:45 +0000
1689@@ -1,7 +1,7 @@
1690 dnl Process this file with autoconf to produce a configure script.
1691 AC_PREREQ(2.53)
1692
1693-AC_INIT([ubuntuone-client], [1.1.4])
1694+AC_INIT([ubuntuone-client], [1.1.90])
1695 AC_CONFIG_SRCDIR([config.h.in])
1696
1697 AM_INIT_AUTOMAKE([1.10 foreign])
1698
1699=== modified file 'contrib/__init__.pyc'
1700Binary files contrib/__init__.pyc 2010-03-10 23:36:53 +0000 and contrib/__init__.pyc 2010-03-31 23:55:45 +0000 differ
1701=== modified file 'contrib/dbus_util.pyc'
1702Binary files contrib/dbus_util.pyc 2010-03-10 23:36:53 +0000 and contrib/dbus_util.pyc 2010-03-31 23:55:45 +0000 differ
1703=== modified file 'contrib/mocker.py'
1704--- contrib/mocker.py 2009-06-30 12:00:00 +0000
1705+++ contrib/mocker.py 2010-03-31 23:55:45 +0000
1706@@ -1092,9 +1092,12 @@
1707
1708 def __nonzero__(self):
1709 try:
1710- return self.__mocker_act__("nonzero")
1711+ result = self.__mocker_act__("nonzero")
1712 except MatchError, e:
1713 return True
1714+ if type(result) is Mock:
1715+ return True
1716+ return result
1717
1718 def __iter__(self):
1719 # XXX On py3k, when next() becomes __next__(), we'll be able
1720
1721=== modified file 'contrib/mocker.pyc'
1722Binary files contrib/mocker.pyc 2010-03-10 23:36:53 +0000 and contrib/mocker.pyc 2010-03-31 23:55:45 +0000 differ
1723=== modified file 'contrib/pylint-wrapper'
1724--- contrib/pylint-wrapper 2009-12-07 17:35:00 +0000
1725+++ contrib/pylint-wrapper 2010-03-31 23:55:45 +0000
1726@@ -71,7 +71,8 @@
1727 # pylint: disable-msg=W0612
1728 for root, dirs, files in os.walk(SRCDIR, topdown=False):
1729 for file in files:
1730- if file.endswith(".py"):
1731+ path = "%s/" % root
1732+ if file.endswith(".py") or path.endswith("bin/"):
1733 pyfiles.append(os.path.join(root, file))
1734
1735 pyfiles.sort()
1736
1737=== modified file 'contrib/testing/__init__.pyc'
1738Binary files contrib/testing/__init__.pyc 2010-03-10 23:36:53 +0000 and contrib/testing/__init__.pyc 2010-03-31 23:55:45 +0000 differ
1739=== modified file 'contrib/testing/testcase.py'
1740--- contrib/testing/testcase.py 2010-03-10 23:36:53 +0000
1741+++ contrib/testing/testcase.py 2010-03-31 23:55:45 +0000
1742@@ -616,6 +616,30 @@
1743 self.remove_from_connection()
1744
1745
1746+class FakeLogger(object):
1747+ """Helper logging class."""
1748+ def __init__(self):
1749+ self.logged = dict(debug=[], warning=[], info=[])
1750+
1751+ def _log(self, log, txt, args):
1752+ """Really logs."""
1753+ if args:
1754+ txt = txt % args
1755+ log.append(txt)
1756+
1757+ def warning(self, txt, *args):
1758+ """WARNING logs."""
1759+ self._log(self.logged['warning'], txt, args)
1760+
1761+ def debug(self, txt, *args):
1762+ """DEBUG logs."""
1763+ self._log(self.logged['debug'], txt, args)
1764+
1765+ def info(self, txt, *args):
1766+ """INFO logs."""
1767+ self._log(self.logged['info'], txt, args)
1768+
1769+
1770 class Listener(object):
1771 """Helper class to gather events."""
1772
1773
1774=== modified file 'contrib/testing/testcase.pyc'
1775Binary files contrib/testing/testcase.pyc 2010-03-10 23:36:53 +0000 and contrib/testing/testcase.pyc 2010-03-31 23:55:45 +0000 differ
1776=== modified file 'data/Makefile.am'
1777--- data/Makefile.am 2010-02-17 23:51:29 +0000
1778+++ data/Makefile.am 2010-03-31 23:55:45 +0000
1779@@ -23,6 +23,10 @@
1780 desktop_in_files = ubuntuone-preferences.desktop.in
1781 desktop_DATA = $(desktop_in_files:.desktop.in=.desktop)
1782
1783+autostartdir = $(sysconfdir)/xdg/autostart
1784+autostart_in_files = ubuntuone-launch.desktop.in
1785+autostart_DATA = $(autostart_in_files:.desktop.in=.desktop)
1786+
1787 @INTLTOOL_DESKTOP_RULE@
1788
1789 %.conf: %.conf.in
1790@@ -132,6 +136,7 @@
1791 $(config_in_files) \
1792 $(memenu_in_files) \
1793 $(desktop_in_files) \
1794+ $(autostart_in_files) \
1795 $(service_in_files) \
1796 $(emblem_in_files) \
1797 $(icon_in_files) \
1798@@ -144,6 +149,7 @@
1799 CLEANFILES = \
1800 $(memenu_DATA) \
1801 $(desktop_DATA) \
1802+ $(autostart_DATA) \
1803 $(service_DATA) \
1804 $(emblem_files) \
1805 hicolor
1806
1807=== modified file 'data/Makefile.in'
1808--- data/Makefile.in 2010-02-17 23:51:29 +0000
1809+++ data/Makefile.in 2010-03-31 23:55:45 +0000
1810@@ -70,12 +70,12 @@
1811 am__base_list = \
1812 sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
1813 sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
1814-am__installdirs = "$(DESTDIR)$(apportdir)" "$(DESTDIR)$(configdir)" \
1815- "$(DESTDIR)$(crashdbdir)" "$(DESTDIR)$(desktopdir)" \
1816- "$(DESTDIR)$(memenudir)" "$(DESTDIR)$(oauthdir)" \
1817- "$(DESTDIR)$(servicedir)"
1818-DATA = $(apport_DATA) $(config_DATA) $(crashdb_DATA) $(desktop_DATA) \
1819- $(memenu_DATA) $(oauth_DATA) $(service_DATA)
1820+am__installdirs = "$(DESTDIR)$(apportdir)" "$(DESTDIR)$(autostartdir)" \
1821+ "$(DESTDIR)$(configdir)" "$(DESTDIR)$(crashdbdir)" \
1822+ "$(DESTDIR)$(desktopdir)" "$(DESTDIR)$(memenudir)" \
1823+ "$(DESTDIR)$(oauthdir)" "$(DESTDIR)$(servicedir)"
1824+DATA = $(apport_DATA) $(autostart_DATA) $(config_DATA) $(crashdb_DATA) \
1825+ $(desktop_DATA) $(memenu_DATA) $(oauth_DATA) $(service_DATA)
1826 DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
1827 ACLOCAL = @ACLOCAL@
1828 ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
1829@@ -237,6 +237,9 @@
1830 desktopdir = $(datadir)/applications
1831 desktop_in_files = ubuntuone-preferences.desktop.in
1832 desktop_DATA = $(desktop_in_files:.desktop.in=.desktop)
1833+autostartdir = $(sysconfdir)/xdg/autostart
1834+autostart_in_files = ubuntuone-launch.desktop.in
1835+autostart_DATA = $(autostart_in_files:.desktop.in=.desktop)
1836 hicolordir = $(datadir)/icons/hicolor
1837 privateiconsdir = $(pkgdatadir)/icons/hicolor
1838 emblem_in_files = \
1839@@ -258,6 +261,7 @@
1840 $(config_in_files) \
1841 $(memenu_in_files) \
1842 $(desktop_in_files) \
1843+ $(autostart_in_files) \
1844 $(service_in_files) \
1845 $(emblem_in_files) \
1846 $(icon_in_files) \
1847@@ -270,6 +274,7 @@
1848 CLEANFILES = \
1849 $(memenu_DATA) \
1850 $(desktop_DATA) \
1851+ $(autostart_DATA) \
1852 $(service_DATA) \
1853 $(emblem_files) \
1854 hicolor
1855@@ -337,6 +342,26 @@
1856 test -n "$$files" || exit 0; \
1857 echo " ( cd '$(DESTDIR)$(apportdir)' && rm -f" $$files ")"; \
1858 cd "$(DESTDIR)$(apportdir)" && rm -f $$files
1859+install-autostartDATA: $(autostart_DATA)
1860+ @$(NORMAL_INSTALL)
1861+ test -z "$(autostartdir)" || $(MKDIR_P) "$(DESTDIR)$(autostartdir)"
1862+ @list='$(autostart_DATA)'; test -n "$(autostartdir)" || list=; \
1863+ for p in $$list; do \
1864+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
1865+ echo "$$d$$p"; \
1866+ done | $(am__base_list) | \
1867+ while read files; do \
1868+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(autostartdir)'"; \
1869+ $(INSTALL_DATA) $$files "$(DESTDIR)$(autostartdir)" || exit $$?; \
1870+ done
1871+
1872+uninstall-autostartDATA:
1873+ @$(NORMAL_UNINSTALL)
1874+ @list='$(autostart_DATA)'; test -n "$(autostartdir)" || list=; \
1875+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
1876+ test -n "$$files" || exit 0; \
1877+ echo " ( cd '$(DESTDIR)$(autostartdir)' && rm -f" $$files ")"; \
1878+ cd "$(DESTDIR)$(autostartdir)" && rm -f $$files
1879 install-configDATA: $(config_DATA)
1880 @$(NORMAL_INSTALL)
1881 test -z "$(configdir)" || $(MKDIR_P) "$(DESTDIR)$(configdir)"
1882@@ -501,7 +526,7 @@
1883 check: check-am
1884 all-am: Makefile $(DATA)
1885 installdirs:
1886- for dir in "$(DESTDIR)$(apportdir)" "$(DESTDIR)$(configdir)" "$(DESTDIR)$(crashdbdir)" "$(DESTDIR)$(desktopdir)" "$(DESTDIR)$(memenudir)" "$(DESTDIR)$(oauthdir)" "$(DESTDIR)$(servicedir)"; do \
1887+ for dir in "$(DESTDIR)$(apportdir)" "$(DESTDIR)$(autostartdir)" "$(DESTDIR)$(configdir)" "$(DESTDIR)$(crashdbdir)" "$(DESTDIR)$(desktopdir)" "$(DESTDIR)$(memenudir)" "$(DESTDIR)$(oauthdir)" "$(DESTDIR)$(servicedir)"; do \
1888 test -z "$$dir" || $(MKDIR_P) "$$dir"; \
1889 done
1890 install: install-am
1891@@ -551,9 +576,10 @@
1892
1893 info-am:
1894
1895-install-data-am: install-apportDATA install-configDATA \
1896- install-crashdbDATA install-data-local install-desktopDATA \
1897- install-memenuDATA install-oauthDATA install-serviceDATA
1898+install-data-am: install-apportDATA install-autostartDATA \
1899+ install-configDATA install-crashdbDATA install-data-local \
1900+ install-desktopDATA install-memenuDATA install-oauthDATA \
1901+ install-serviceDATA
1902
1903 install-dvi: install-dvi-am
1904
1905@@ -598,29 +624,31 @@
1906
1907 ps-am:
1908
1909-uninstall-am: uninstall-apportDATA uninstall-configDATA \
1910- uninstall-crashdbDATA uninstall-desktopDATA uninstall-local \
1911- uninstall-memenuDATA uninstall-oauthDATA uninstall-serviceDATA
1912+uninstall-am: uninstall-apportDATA uninstall-autostartDATA \
1913+ uninstall-configDATA uninstall-crashdbDATA \
1914+ uninstall-desktopDATA uninstall-local uninstall-memenuDATA \
1915+ uninstall-oauthDATA uninstall-serviceDATA
1916
1917 .MAKE: install-am install-strip
1918
1919 .PHONY: all all-am check check-am clean clean-generic clean-libtool \
1920 dist-hook distclean distclean-generic distclean-libtool \
1921 distdir dvi dvi-am html html-am info info-am install \
1922- install-am install-apportDATA install-configDATA \
1923- install-crashdbDATA install-data install-data-am \
1924- install-data-local install-desktopDATA install-dvi \
1925- install-dvi-am install-exec install-exec-am install-html \
1926- install-html-am install-info install-info-am install-man \
1927- install-memenuDATA install-oauthDATA install-pdf \
1928+ install-am install-apportDATA install-autostartDATA \
1929+ install-configDATA install-crashdbDATA install-data \
1930+ install-data-am install-data-local install-desktopDATA \
1931+ install-dvi install-dvi-am install-exec install-exec-am \
1932+ install-html install-html-am install-info install-info-am \
1933+ install-man install-memenuDATA install-oauthDATA install-pdf \
1934 install-pdf-am install-ps install-ps-am install-serviceDATA \
1935 install-strip installcheck installcheck-am installdirs \
1936 maintainer-clean maintainer-clean-generic \
1937 maintainer-clean-local mostlyclean mostlyclean-generic \
1938 mostlyclean-libtool pdf pdf-am ps ps-am uninstall uninstall-am \
1939- uninstall-apportDATA uninstall-configDATA \
1940- uninstall-crashdbDATA uninstall-desktopDATA uninstall-local \
1941- uninstall-memenuDATA uninstall-oauthDATA uninstall-serviceDATA
1942+ uninstall-apportDATA uninstall-autostartDATA \
1943+ uninstall-configDATA uninstall-crashdbDATA \
1944+ uninstall-desktopDATA uninstall-local uninstall-memenuDATA \
1945+ uninstall-oauthDATA uninstall-serviceDATA
1946
1947
1948 %.menu: %.menu.in
1949
1950=== modified file 'data/oauth_registration.d/ubuntuone'
1951--- data/oauth_registration.d/ubuntuone 2009-06-30 12:00:00 +0000
1952+++ data/oauth_registration.d/ubuntuone 2010-03-31 23:55:45 +0000
1953@@ -10,4 +10,14 @@
1954 exe_path = /usr/bin/python
1955 application_name = ubuntuone-syncdaemon
1956
1957+[control-panel]
1958+realm = https://ubuntuone.com
1959+consumer_key = ubuntuone
1960+exe_path = /usr/bin/python
1961+application_name = ubuntuone-preferences
1962
1963+[launcher]
1964+realm = https://ubuntuone.com
1965+consumer_key = ubuntuone
1966+exe_path = /usr/bin/python
1967+application_name = ubuntuone-launch
1968
1969=== modified file 'data/source_ubuntuone-client.py'
1970--- data/source_ubuntuone-client.py 2009-09-28 18:15:00 +0000
1971+++ data/source_ubuntuone-client.py 2010-03-31 23:55:45 +0000
1972@@ -27,8 +27,10 @@
1973 # things we may want to collect for the report
1974 u1_client_log = os.path.join(u1_log_path, "syncdaemon.log")
1975 u1_except_log = os.path.join(u1_log_path, "syncdaemon-exceptions.log")
1976+u1_invalidnames_log = os.path.join(u1_log_path, "syncdaemon-invalid-names.log")
1977 u1_oauth_log = os.path.join(u1_log_path, "oauth-login.log")
1978 u1_u1sync_log = os.path.join(u1_log_path, "u1sync.log")
1979+u1_prefs_log = os.path.join(u1_log_path, "u1-prefs.log")
1980 u1_sd_conf = os.path.join("etc", "xdg", "ubuntuone", "syncdaemon.conf")
1981 u1_usersd_conf = os.path.join(u1_user_config_path, "syncdaemon.conf")
1982 u1_user_conf = os.path.join(u1_user_config_path, "ubuntuone-client.conf")
1983@@ -36,11 +38,20 @@
1984
1985 def add_info(report):
1986 """add report info"""
1987- attach_file_if_exists(report, u1_except_log)
1988- attach_file_if_exists(report, u1_oauth_log)
1989- attach_file_if_exists(report, u1_usersd_conf)
1990- attach_file_if_exists(report, u1_sd_conf)
1991- attach_file_if_exists(report, u1_user_conf)
1992+ attach_file_if_exists(report, u1_except_log,
1993+ "UbuntuOneSyncdaemonExceptionsLog")
1994+ attach_file_if_exists(report, u1_invalidnames_log,
1995+ "UbuntuOneSyncdaemonInvalidNamesLog")
1996+ attach_file_if_exists(report, u1_oauth_log,
1997+ "UbuntuOneOAuthLoginLog")
1998+ attach_file_if_exists(report, u1_prefs_log,
1999+ "UbuntuOnePreferencesLog")
2000+ attach_file_if_exists(report, u1_usersd_conf,
2001+ "UbuntuOneSyncdaemonConfig")
2002+ attach_file_if_exists(report, u1_sd_conf,
2003+ "UbuntuOneUserSyncdaemonConfig")
2004+ attach_file_if_exists(report, u1_user_conf,
2005+ "UbuntuOneClientConfig")
2006
2007 if not apport.packaging.is_distro_package(report['Package'].split()[0]):
2008 report['ThirdParty'] = 'True'
2009@@ -62,4 +73,4 @@
2010 if version is None:
2011 version = 'N/A'
2012 versions += '%s %s\n' % (package, version)
2013- report['UbuntuoneClientPackages'] = versions
2014+ report['UbuntuOneClientPackages'] = versions
2015
2016=== modified file 'data/syncdaemon.conf'
2017--- data/syncdaemon.conf 2010-03-04 16:47:43 +0000
2018+++ data/syncdaemon.conf 2010-03-31 23:55:45 +0000
2019@@ -14,6 +14,11 @@
2020 port.parser = int
2021 port.help = The port on which to connect to the server
2022
2023+files_sync_enabled.default = True
2024+files_sync_enabled.action = store_true
2025+files_sync_enabled.parser = bool
2026+files_sync_enabled.help = Toggles synchronization of files (False disables syncdaemon entirely)
2027+
2028 root_dir.default = ~/Ubuntu One
2029 root_dir.parser = home_dir
2030 root_dir.help = Use the specified directory as the root
2031@@ -62,6 +67,17 @@
2032 udf_autosubscribe.help = Autosubsribe to new User Defined Folders, 'on' by default.
2033 (accepted values: 1/0, on/off, true/false and yes/no)
2034
2035+ignore.parser = lines
2036+ignore.help = The list of (Python, not bash) regexes of the files that
2037+ SD should ignore.
2038+ignore.default = \A#.*\Z
2039+ \A.*~\Z
2040+ \A.*\.py[oc]\Z
2041+ \A.*\.sw[nop]\Z
2042+ \A.*\.swpx\Z
2043+ \A\..*\.tmp\Z
2044+
2045+
2046 [bandwidth_throttling]
2047 on.default = False
2048 on.parser = bool
2049
2050=== added file 'data/ubuntuone-launch.desktop.in'
2051--- data/ubuntuone-launch.desktop.in 1970-01-01 00:00:00 +0000
2052+++ data/ubuntuone-launch.desktop.in 2010-03-31 23:55:45 +0000
2053@@ -0,0 +1,7 @@
2054+[Desktop Entry]
2055+Name=Ubuntu One
2056+Exec=/bin/sh -c '[ -d "$HOME/Ubuntu One" ] && ubuntuone-launch'
2057+Type=Application
2058+X-GNOME-Autostart-Delay=30
2059+Icon=ubuntuone
2060+Comment= 
2061
2062=== modified file 'data/ubuntuone-preferences.desktop.in'
2063--- data/ubuntuone-preferences.desktop.in 2010-02-17 23:51:29 +0000
2064+++ data/ubuntuone-preferences.desktop.in 2010-03-31 23:55:45 +0000
2065@@ -1,6 +1,6 @@
2066 [Desktop Entry]
2067 Name=Ubuntu One
2068-Comment=Configure and manage your Ubuntu One account
2069+_Comment=Configure and manage your Ubuntu One account
2070 Exec=ubuntuone-preferences
2071 Icon=ubuntuone
2072 Terminal=false
2073
2074=== modified file 'debian/changelog'
2075--- debian/changelog 2010-03-11 00:04:35 +0000
2076+++ debian/changelog 2010-03-31 23:55:45 +0000
2077@@ -1,3 +1,16 @@
2078+ubuntuone-client (1.1.90-0ubuntu1) UNRELEASED; urgency=low
2079+
2080+ * New upstream release.
2081+ - Notify user when approaching and exceeding quota (LP: #540360)
2082+ - Add instructional text to ubuntuone-preferences (LP: #539676)
2083+ - UbuntuOne needs to autostart and connect by defualt (LP: #534707)
2084+ - Impossible to infer status of file synchronization (LP: #526084)
2085+ - Devices and Services tabs functionality and development (LP: #525803)
2086+ * Add debian/source/format.
2087+ * Remove python-httplib2 from the dependencies (LP: #535207)
2088+
2089+ -- Rodney Dawes <rodney.dawes@canonical.com> Wed, 31 Mar 2010 19:47:01 -0400
2090+
2091 ubuntuone-client (1.1.4-0ubuntu1) lucid; urgency=low
2092
2093 * New upstream release.
2094
2095=== modified file 'debian/control'
2096--- debian/control 2010-03-10 23:50:45 +0000
2097+++ debian/control 2010-03-31 23:55:45 +0000
2098@@ -41,7 +41,6 @@
2099 Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends},
2100 ubuntuone-client (= ${source:Version}),
2101 python-gtk2 (>= 2.10),
2102- python-httplib2,
2103 python-simplejson
2104 Replaces: ubuntuone-client (<= 1.1.1)
2105 Conflicts: ubuntuone-client (<= 1.1.1)
2106
2107=== added directory 'debian/source'
2108=== added file 'debian/source/format'
2109--- debian/source/format 1970-01-01 00:00:00 +0000
2110+++ debian/source/format 2010-03-31 23:55:45 +0000
2111@@ -0,0 +1,2 @@
2112+3.0 (quilt)
2113+
2114
2115=== modified file 'docs/states_manager.svg'
2116--- docs/states_manager.svg 2010-03-04 16:47:43 +0000
2117+++ docs/states_manager.svg 2010-03-31 23:55:45 +0000
2118@@ -33,10 +33,10 @@
2119 inkscape:document-units="px"
2120 inkscape:current-layer="layer1"
2121 showgrid="false"
2122- inkscape:window-width="1280"
2123- inkscape:window-height="728"
2124+ inkscape:window-width="1920"
2125+ inkscape:window-height="1010"
2126 inkscape:window-x="0"
2127- inkscape:window-y="25"
2128+ inkscape:window-y="24"
2129 inkscape:window-maximized="1"
2130 showguides="true"
2131 inkscape:guide-bbox="true" />
2132@@ -5733,6 +5733,46 @@
2133 inkscape:vp_y="0 : 1000 : 0"
2134 inkscape:vp_x="0 : 0.5 : 1"
2135 sodipodi:type="inkscape:persp3d" />
2136+ <inkscape:perspective
2137+ id="perspective4002"
2138+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
2139+ inkscape:vp_z="1 : 0.5 : 1"
2140+ inkscape:vp_y="0 : 1000 : 0"
2141+ inkscape:vp_x="0 : 0.5 : 1"
2142+ sodipodi:type="inkscape:persp3d" />
2143+ <marker
2144+ inkscape:stockid="Arrow1Mend1u64"
2145+ orient="auto"
2146+ refY="0"
2147+ refX="0"
2148+ id="Arrow1Mend1u64-857"
2149+ style="overflow:visible">
2150+ <path
2151+ id="path7114-41"
2152+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 z"
2153+ style="fill:#796a69;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
2154+ transform="matrix(-0.4,0,0,-0.4,-4,0)" />
2155+ </marker>
2156+ <inkscape:perspective
2157+ id="perspective4069"
2158+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
2159+ inkscape:vp_z="1 : 0.5 : 1"
2160+ inkscape:vp_y="0 : 1000 : 0"
2161+ inkscape:vp_x="0 : 0.5 : 1"
2162+ sodipodi:type="inkscape:persp3d" />
2163+ <marker
2164+ inkscape:stockid="Arrow1Mend1u64"
2165+ orient="auto"
2166+ refY="0"
2167+ refX="0"
2168+ id="Arrow1Mend1u64-64"
2169+ style="overflow:visible">
2170+ <path
2171+ id="path7114-30"
2172+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 z"
2173+ style="fill:#796a69;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
2174+ transform="matrix(-0.4,0,0,-0.4,-4,0)" />
2175+ </marker>
2176 </defs>
2177 <metadata
2178 id="metadata7">
2179@@ -5849,63 +5889,63 @@
2180 <text
2181 sodipodi:linespacing="125%"
2182 xml:space="preserve"
2183- style="font-size:19.21366692px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2184- x="751.61261"
2185- y="556.11877"
2186+ style="font-size:17.80227852px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2187+ x="839.35968"
2188+ y="460.50702"
2189 id="text2467-1-5-4-7"><tspan
2190 sodipodi:role="line"
2191 id="tspan2469-4-3-8-9"
2192- x="751.61261"
2193- y="556.11877"
2194- style="font-size:12.22687912px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">3. SYS_NET_CONNECTED</tspan></text>
2195+ x="839.35968"
2196+ y="460.50702"
2197+ style="font-size:11.328722px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">3. SYS_NET_CONNECTED</tspan></text>
2198 <text
2199 sodipodi:linespacing="125%"
2200 xml:space="preserve"
2201- style="font-size:19.21366692px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2202- x="751.82153"
2203- y="576.80383"
2204+ style="font-size:17.80227852px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2205+ x="839.55328"
2206+ y="479.67267"
2207 id="text2467-1-5-4-4"><tspan
2208 sodipodi:role="line"
2209 id="tspan2469-4-3-8-1"
2210- x="751.82153"
2211- y="576.80383"
2212- style="font-size:12.22687912px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">4. SYS_USER_CONNECT</tspan></text>
2213+ x="839.55328"
2214+ y="479.67267"
2215+ style="font-size:11.328722px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">4. SYS_USER_CONNECT</tspan></text>
2216 <text
2217 sodipodi:linespacing="125%"
2218 xml:space="preserve"
2219- style="font-size:19.21366692px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2220- x="751.60071"
2221- y="638.85907"
2222+ style="font-size:17.80227852px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2223+ x="839.34869"
2224+ y="537.16943"
2225 id="text2467-1-5-4-5"><tspan
2226 sodipodi:role="line"
2227 id="tspan2469-4-3-8-3"
2228- x="751.60071"
2229- y="638.85907"
2230- style="font-size:12.22687912px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">7. SYS_CONNECTION_LOST</tspan></text>
2231+ x="839.34869"
2232+ y="537.16943"
2233+ style="font-size:11.328722px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">7. SYS_CONNECTION_LOST</tspan></text>
2234 <text
2235 sodipodi:linespacing="125%"
2236 xml:space="preserve"
2237- style="font-size:19.21366692px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2238- x="751.57678"
2239- y="597.48889"
2240+ style="font-size:17.80227852px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2241+ x="839.32654"
2242+ y="498.8382"
2243 id="text2467-1-5-4-3-8"><tspan
2244 sodipodi:role="line"
2245 id="tspan2469-4-3-8-38"
2246- x="751.57678"
2247- y="597.48889"
2248- style="font-size:12.22687912px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">5. SYS_NET_DISCONNECTED</tspan></text>
2249+ x="839.32654"
2250+ y="498.8382"
2251+ style="font-size:11.328722px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">5. SYS_NET_DISCONNECTED</tspan></text>
2252 <text
2253 sodipodi:linespacing="125%"
2254 xml:space="preserve"
2255- style="font-size:19.21366692px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2256- x="751.63654"
2257- y="618.17401"
2258+ style="font-size:17.80227852px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2259+ x="839.38184"
2260+ y="518.00385"
2261 id="text2467-1-5-4-4-0"><tspan
2262 sodipodi:role="line"
2263 id="tspan2469-4-3-8-1-3"
2264- x="751.63654"
2265- y="618.17401"
2266- style="font-size:12.22687912px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">6. SYS_USER_DISCONNECT</tspan></text>
2267+ x="839.38184"
2268+ y="518.00385"
2269+ style="font-size:11.328722px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">6. SYS_USER_DISCONNECT</tspan></text>
2270 <g
2271 id="g8266"
2272 transform="matrix(0.74658494,0,0,0.74658494,-0.8603134,-224.75307)">
2273@@ -6138,27 +6178,6 @@
2274 x="373.00955"
2275 sodipodi:role="line"
2276 id="tspan8676-1-0">AUTH_FAILED</tspan></text>
2277- <g
2278- id="g8763"
2279- transform="translate(-571.53861,-16.17932)">
2280- <path
2281- sodipodi:nodetypes="ccccccc"
2282- id="rect8502-1-3-1-3"
2283- d="m 687.97895,686.92944 19.03124,19.03125 -18.43749,18.4375 130.95537,0 -19.03125,-19.03125 18.4375,-18.4375 -130.95537,0 z"
2284- style="opacity:0.80373798;color:#000000;fill:#ececec;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.33651051;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
2285- <text
2286- sodipodi:linespacing="150%"
2287- xml:space="preserve"
2288- style="font-size:11.40619183px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:150%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans"
2289- x="753.75067"
2290- y="709.81586"
2291- id="text2429-9-7-7-2-2-4-3"><tspan
2292- id="tspan8676-1-9"
2293- sodipodi:role="line"
2294- x="753.75067"
2295- y="709.81586"
2296- style="font-size:11.40619183px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:150%;writing-mode:lr-tb;text-anchor:middle;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans">UNKNOWN</tspan></text>
2297- </g>
2298 <path
2299 style="color:#000000;fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:url(#Arrow1Mend);visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2300 d="M 196.60308,507.23994 332.40207,379.84707"
2301@@ -6498,15 +6517,15 @@
2302 <text
2303 sodipodi:linespacing="125%"
2304 xml:space="preserve"
2305- style="font-size:19.21366692px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2306- x="751.64844"
2307- y="659.54419"
2308+ style="font-size:17.80227852px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2309+ x="839.39288"
2310+ y="556.33508"
2311 id="text2467-1-5-4-5-5"><tspan
2312 sodipodi:role="line"
2313 id="tspan2469-4-3-8-3-7"
2314- x="751.64844"
2315- y="659.54419"
2316- style="font-size:12.22687912px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">8. SYS_HANDSHAKE_TIMEOUT</tspan></text>
2317+ x="839.39288"
2318+ y="556.33508"
2319+ style="font-size:11.328722px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">8. SYS_HANDSHAKE_TIMEOUT</tspan></text>
2320 <path
2321 style="color:#000000;fill:none;stroke:#000000;stroke-width:1.50000000000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-start:url(#DotM);marker-end:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2322 d="m 172.15675,355.01368 21.74975,7.69663"
2323@@ -6599,22 +6618,6 @@
2324 style="opacity:0.65322583;color:#000000;fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2325 sodipodi:type="arc" />
2326 <path
2327- style="color:#000000;fill:none;stroke:#967b71;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2328- d="M 41.694143,630.27619 C 191.18408,629.59254 179.79003,629.79207 183.20824,666.48091"
2329- id="path8817-5-8-1-5-2"
2330- sodipodi:nodetypes="cc" />
2331- <text
2332- sodipodi:linespacing="125%"
2333- xml:space="preserve"
2334- style="font-size:12.56705952px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic"
2335- x="41.174591"
2336- y="625.82983"
2337- id="text2461-6-0-31-6-2"><tspan
2338- sodipodi:role="line"
2339- x="41.174591"
2340- y="625.82983"
2341- id="tspan2463-3-4-2-8-1">From any node</tspan></text>
2342- <path
2343 transform="matrix(0.24411038,0,0,0.22458156,264.46156,450.51532)"
2344 d="m -147.4098,407.55515 a 42.646858,47.282387 0 1 1 -85.29371,0 42.646858,47.282387 0 1 1 85.29371,0 z"
2345 sodipodi:ry="47.282387"
2346@@ -6635,16 +6638,6 @@
2347 style="opacity:0.65322583;color:#000000;fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2348 sodipodi:type="arc" />
2349 <path
2350- transform="matrix(0.24411038,0,0,0.22458156,229.17074,569.22208)"
2351- d="m -147.4098,407.55515 a 42.646858,47.282387 0 1 1 -85.29371,0 42.646858,47.282387 0 1 1 85.29371,0 z"
2352- sodipodi:ry="47.282387"
2353- sodipodi:rx="42.646858"
2354- sodipodi:cy="407.55515"
2355- sodipodi:cx="-190.05666"
2356- id="path4627-2-8-0"
2357- style="opacity:0.65322583;color:#000000;fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2358- sodipodi:type="arc" />
2359- <path
2360 transform="matrix(0.24411038,0,0,0.22458156,644.85465,391.20219)"
2361 d="m -147.4098,407.55515 a 42.646858,47.282387 0 1 1 -85.29371,0 42.646858,47.282387 0 1 1 85.29371,0 z"
2362 sodipodi:ry="47.282387"
2363@@ -6767,26 +6760,26 @@
2364 <text
2365 sodipodi:linespacing="125%"
2366 xml:space="preserve"
2367- style="font-size:10.45871924999999969px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:end;text-decoration:none;line-height:125%;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:end;color:#000000;fill:#ff9d5b;fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic;opacity:1"
2368- x="944.04236"
2369- y="682.54211"
2370+ style="font-size:9.69044781px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:end;text-decoration:none;line-height:125%;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:end;color:#000000;fill:#ff9d5b;fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic"
2371+ x="1017.654"
2372+ y="577.64355"
2373 id="text2461-6-0-31-6-4-5"><tspan
2374 sodipodi:role="line"
2375- x="944.04236"
2376- y="682.54211"
2377+ x="1017.654"
2378+ y="577.64355"
2379 id="tspan18329"
2380- style="font-size:10.45871924999999969px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:end;text-decoration:none;line-height:125%;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:end;color:#000000;fill:#ff9d5b;fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic">Note: On all nodes 3 &amp; 4 do anything</tspan></text>
2381+ style="font-size:9.69044781px;font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:end;text-decoration:none;line-height:125%;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:end;color:#000000;fill:#ff9d5b;fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic">Note: On all nodes 3 &amp; 4 do anything</tspan></text>
2382 <text
2383 sodipodi:linespacing="125%"
2384 xml:space="preserve"
2385- style="font-size:10.45871924999999969px;font-style:italic;font-weight:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic"
2386- x="-551.41644"
2387- y="739.90375"
2388+ style="font-size:9.69044781px;font-style:italic;font-weight:normal;text-align:end;line-height:125%;writing-mode:lr-tb;text-anchor:end;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic"
2389+ x="-456.15012"
2390+ y="828.51099"
2391 id="text2461-6-0-31-6-4-5-3"
2392 transform="matrix(0,-1,1,0,0,0)"><tspan
2393 sodipodi:role="line"
2394- x="-551.41644"
2395- y="739.90375"
2396+ x="-456.15012"
2397+ y="828.51099"
2398 id="tspan18329-0">ConnectionManager</tspan></text>
2399 <g
2400 id="g18554"
2401@@ -6966,30 +6959,6 @@
2402 x="1138.0615"
2403 sodipodi:role="line">AQ.disconnect()</tspan></text>
2404 </g>
2405- <g
2406- id="g18776"
2407- transform="translate(-1317.1307,149.4903)">
2408- <rect
2409- style="opacity:0.65322583;color:#000000;fill:#00ff00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.847;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2410- id="rect5912-2-48-2-9-0"
2411- width="129.83394"
2412- height="19.030485"
2413- x="1372.1442"
2414- y="549.65802"
2415- ry="4.5156531"
2416- rx="4.5156531" />
2417- <text
2418- id="text2461-6-7-4-5-0"
2419- y="562.5957"
2420- x="1399.2255"
2421- style="font-size:10.90068817px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans"
2422- xml:space="preserve"
2423- sodipodi:linespacing="125%"><tspan
2424- id="tspan2463-3-5-2-7-1"
2425- y="562.5957"
2426- x="1399.2255"
2427- sodipodi:role="line">main.restart()</tspan></text>
2428- </g>
2429 <text
2430 sodipodi:linespacing="125%"
2431 xml:space="preserve"
2432@@ -7383,18 +7352,6 @@
2433 <text
2434 sodipodi:linespacing="125%"
2435 xml:space="preserve"
2436- style="font-size:16.76553154px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;color:#000000;fill:#967b71;fill-opacity:1;stroke:none;stroke-width:2;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2437- x="102.40143"
2438- y="647.05518"
2439- id="text2467-1-5-4-7-3-64-9-9"><tspan
2440- sodipodi:role="line"
2441- id="tspan2469-4-3-8-9-0-4-4-7"
2442- x="102.40143"
2443- y="647.05518"
2444- style="font-size:10.66897488px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;color:#000000;fill:#967b71;fill-opacity:1;stroke:none;stroke-width:2;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">SYS_UNKNOWN_ERROR</tspan></text>
2445- <text
2446- sodipodi:linespacing="125%"
2447- xml:space="preserve"
2448 style="font-size:25.45085716px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2449 x="234.69305"
2450 y="438.2403"
2451@@ -7404,5 +7361,140 @@
2452 x="234.69305"
2453 y="438.2403"
2454 style="font-size:16.19600105px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff6600;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">5,6</tspan></text>
2455+ <g
2456+ style="display:inline"
2457+ id="g8763-8"
2458+ transform="matrix(0.76886496,0,0,0.76886496,230.74978,145.34928)">
2459+ <path
2460+ sodipodi:nodetypes="ccccccc"
2461+ id="rect8502-1-3-1-3-5"
2462+ d="m 687.97895,686.92944 19.03124,19.03125 -18.43749,18.4375 130.95537,0 -19.03125,-19.03125 18.4375,-18.4375 -130.95537,0 z"
2463+ style="opacity:0.80373798;color:#000000;fill:#ececec;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.33651051;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
2464+ <text
2465+ sodipodi:linespacing="150%"
2466+ xml:space="preserve"
2467+ style="font-size:11.40619183px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:150%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans"
2468+ x="753.75067"
2469+ y="709.81586"
2470+ id="text2429-9-7-7-2-2-4-3-9"><tspan
2471+ id="tspan8676-1-9-7"
2472+ sodipodi:role="line"
2473+ x="753.75067"
2474+ y="709.81586"
2475+ style="font-size:11.40619183px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:150%;writing-mode:lr-tb;text-anchor:middle;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans">UNKNOWN</tspan></text>
2476+ </g>
2477+ <path
2478+ style="color:#000000;fill:none;stroke:#967b71;stroke-width:1.53772998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2479+ d="m 702.24295,642.38627 c 114.93757,-0.52564 106.17709,-0.37222 108.80523,27.83654"
2480+ id="path8817-5-8-1-5-2-5"
2481+ sodipodi:nodetypes="cc" />
2482+ <text
2483+ sodipodi:linespacing="125%"
2484+ xml:space="preserve"
2485+ style="font-size:9.66237164px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic"
2486+ x="701.84351"
2487+ y="638.96759"
2488+ id="text2461-6-0-31-6-2-3"><tspan
2489+ sodipodi:role="line"
2490+ x="701.84351"
2491+ y="638.96759"
2492+ id="tspan2463-3-4-2-8-1-8">From any node</tspan></text>
2493+ <path
2494+ transform="matrix(0.18768792,0,0,0.17267289,846.38714,595.4439)"
2495+ d="m -147.4098,407.55515 a 42.646858,47.282387 0 1 1 -85.29371,0 42.646858,47.282387 0 1 1 85.29371,0 z"
2496+ sodipodi:ry="47.282387"
2497+ sodipodi:rx="42.646858"
2498+ sodipodi:cy="407.55515"
2499+ sodipodi:cx="-190.05666"
2500+ id="path4627-2-8-0-8"
2501+ style="opacity:0.65322583;color:#000000;fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2502+ sodipodi:type="arc" />
2503+ <g
2504+ style="display:inline"
2505+ id="g18776-3"
2506+ transform="matrix(0.76886496,0,0,0.76886496,-342.50986,272.72685)">
2507+ <rect
2508+ style="opacity:0.65322583;color:#000000;fill:#00ff00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.847;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2509+ id="rect5912-2-48-2-9-0-1"
2510+ width="129.83394"
2511+ height="19.030485"
2512+ x="1372.1442"
2513+ y="549.65802"
2514+ ry="4.5156531"
2515+ rx="4.5156531" />
2516+ <text
2517+ id="text2461-6-7-4-5-0-8"
2518+ y="562.5957"
2519+ x="1399.2255"
2520+ style="font-size:10.90068817px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans"
2521+ xml:space="preserve"
2522+ sodipodi:linespacing="125%"><tspan
2523+ id="tspan2463-3-5-2-7-1-9"
2524+ y="562.5957"
2525+ x="1399.2255"
2526+ sodipodi:role="line">main.restart()</tspan></text>
2527+ </g>
2528+ <text
2529+ sodipodi:linespacing="125%"
2530+ xml:space="preserve"
2531+ style="font-size:12.8904295px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;color:#000000;fill:#967b71;fill-opacity:1;stroke:none;stroke-width:2;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2532+ x="748.91864"
2533+ y="655.28705"
2534+ id="text2467-1-5-4-7-3-64-9-9-6"><tspan
2535+ sodipodi:role="line"
2536+ id="tspan2469-4-3-8-9-0-4-4-7-4"
2537+ x="748.91864"
2538+ y="655.28705"
2539+ style="font-size:8.20300102px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;color:#000000;fill:#967b71;fill-opacity:1;stroke:none;stroke-width:2;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">SYS_UNKNOWN_ERROR</tspan></text>
2540+ <g
2541+ style="display:inline"
2542+ id="g8763-8-3"
2543+ transform="matrix(0.76886496,0,0,0.76886496,398.58213,145.34928)">
2544+ <path
2545+ sodipodi:nodetypes="ccccccc"
2546+ id="rect8502-1-3-1-3-5-0"
2547+ d="m 687.97895,686.92944 19.03124,19.03125 -18.43749,18.4375 130.95537,0 -19.03125,-19.03125 18.4375,-18.4375 -130.95537,0 z"
2548+ style="opacity:0.80373798;color:#000000;fill:#ececec;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.33651051;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
2549+ <text
2550+ sodipodi:linespacing="150%"
2551+ xml:space="preserve"
2552+ style="font-size:11.40619183px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:150%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans"
2553+ x="753.75067"
2554+ y="709.81586"
2555+ id="text2429-9-7-7-2-2-4-3-9-9"><tspan
2556+ id="tspan8676-1-9-7-2"
2557+ sodipodi:role="line"
2558+ x="753.75067"
2559+ y="709.81586"
2560+ style="font-size:11.40619183px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:150%;writing-mode:lr-tb;text-anchor:middle;font-family:DejaVu Sans;-inkscape-font-specification:DejaVu Sans">ROOT_MISMATCH</tspan></text>
2561+ </g>
2562+ <path
2563+ style="color:#000000;fill:none;stroke:#967b71;stroke-width:1.53772998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
2564+ d="m 870.0753,642.38627 c 114.93757,-0.52564 106.17709,-0.37222 108.80523,27.83654"
2565+ id="path8817-5-8-1-5-2-5-5"
2566+ sodipodi:nodetypes="cc" />
2567+ <text
2568+ sodipodi:linespacing="125%"
2569+ xml:space="preserve"
2570+ style="font-size:9.66237164px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans Italic"
2571+ x="869.67584"
2572+ y="638.96759"
2573+ id="text2461-6-0-31-6-2-3-4"><tspan
2574+ sodipodi:role="line"
2575+ x="869.67584"
2576+ y="638.96759"
2577+ id="tspan2463-3-4-2-8-1-8-0">From any node</tspan></text>
2578+ <text
2579+ sodipodi:linespacing="125%"
2580+ xml:space="preserve"
2581+ style="font-size:12.8904295px;font-style:italic;font-weight:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;color:#000000;fill:#967b71;fill-opacity:1;stroke:none;stroke-width:2;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans;-inkscape-font-specification:Sans"
2582+ x="916.75098"
2583+ y="655.28705"
2584+ id="text2467-1-5-4-7-3-64-9-9-6-2"><tspan
2585+ sodipodi:role="line"
2586+ id="tspan2469-4-3-8-9-0-4-4-7-4-2"
2587+ x="916.75098"
2588+ y="655.28705"
2589+ style="font-size:8.20300102px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;color:#000000;fill:#967b71;fill-opacity:1;stroke:none;stroke-width:2;marker:none;marker-end:url(#Arrow1Mend1u64);visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Bitstream Vera Sans Mono;-inkscape-font-specification:Bitstream Vera Sans Mono">SYS_ROOT_MISMATCH</tspan></text>
2590 </g>
2591 </svg>
2592
2593=== modified file 'docs/syncdaemon_dbus_api.txt'
2594--- docs/syncdaemon_dbus_api.txt 2010-03-10 23:36:53 +0000
2595+++ docs/syncdaemon_dbus_api.txt 2010-03-31 23:55:45 +0000
2596@@ -48,6 +48,9 @@
2597 UploadFinished(path=s,info=a{ss})
2598 Fire a D-BUS signal, notifying an upload has finished.
2599
2600+ InvalidName(dirname=s,filename=ay)
2601+ Fire a D-BUS signal, notifying an invalid file or dir name.
2602+
2603 DownloadStarted(path=v)
2604 Fire a D-BUS signal, notifying a download has started.
2605
2606@@ -141,6 +144,9 @@
2607 udf_autosubscribe_enabled()
2608 Return the udf_autosubscribe config value.
2609
2610+ files_sync_enabled()
2611+ No docstring
2612+
2613 disable_bandwidth_throttling()
2614 Disable bandwidth throttling.
2615
2616@@ -154,6 +160,9 @@
2617 enable_bandwidth_throttling()
2618 Enable bandwidth throttling.
2619
2620+ set_files_sync_enabled(enabled=b)
2621+ Enable UDF autosubscribe.
2622+
2623 disable_udf_autosubscribe()
2624 Enable UDF autosubscribe.
2625
2626
2627=== modified file 'nautilus/contacts-view.c'
2628--- nautilus/contacts-view.c 2010-02-17 23:51:29 +0000
2629+++ nautilus/contacts-view.c 2010-03-31 23:55:45 +0000
2630@@ -38,6 +38,7 @@
2631 } SelectedContactInfo;
2632
2633 enum {
2634+ SELECTION_CHANGED_SIGNAL,
2635 CONTACTS_COUNT_CHANGED_SIGNAL,
2636 LAST_SIGNAL
2637 };
2638@@ -85,6 +86,15 @@
2639 object_class->finalize = contacts_view_finalize;
2640
2641 /* Signals */
2642+ contacts_view_signals[SELECTION_CHANGED_SIGNAL] =
2643+ g_signal_new ("selection-changed",
2644+ G_OBJECT_CLASS_TYPE (object_class),
2645+ G_SIGNAL_RUN_LAST,
2646+ G_STRUCT_OFFSET (ContactsViewClass, selection_changed),
2647+ NULL, NULL,
2648+ g_cclosure_marshal_VOID__VOID,
2649+ G_TYPE_NONE,
2650+ 0);
2651 contacts_view_signals[CONTACTS_COUNT_CHANGED_SIGNAL] =
2652 g_signal_new ("contacts-count-changed",
2653 G_OBJECT_CLASS_TYPE (object_class),
2654@@ -115,38 +125,33 @@
2655 }
2656
2657 static void
2658-item_activated_cb (GtkIconView *icon_view, GtkTreePath *path, gpointer data)
2659+selection_changed_cb (GtkIconView *icon_view, gpointer data)
2660 {
2661- GtkTreeIter iter;
2662 GtkTreeModel *model;
2663+ GList *selected_items, *l;
2664 ContactsView *cv = CONTACTS_VIEW (data);
2665
2666+ g_debug ("Selection_changed called");
2667+
2668+ /* We first remove all the previous selected items */
2669+ g_hash_table_remove_all (cv->selection);
2670+
2671+ /* Now add the new selection */
2672 model = gtk_icon_view_get_model (icon_view);
2673- if (gtk_tree_model_get_iter (model, &iter, path)) {
2674- gchar *name, *email;
2675- GdkPixbuf *icon;
2676-
2677- gtk_tree_model_get (model, &iter,
2678- CONTACTS_VIEW_COLUMN_NAME, &name,
2679- CONTACTS_VIEW_COLUMN_EMAIL, &email,
2680- CONTACTS_VIEW_COLUMN_PIXBUF, &icon,
2681- -1);
2682-
2683- /* if already selected, deselect it */
2684- if (g_hash_table_lookup (cv->selection, name)) {
2685- gtk_list_store_set (GTK_LIST_STORE (model), &iter,
2686- CONTACTS_VIEW_COLUMN_MARKUP, name,
2687- -1);
2688- g_hash_table_remove (cv->selection, name);
2689- } else {
2690- gchar *markup;
2691+ selected_items = gtk_icon_view_get_selected_items (icon_view);
2692+ for (l = selected_items; l != NULL; l = l->next) {
2693+ GtkTreeIter iter;
2694+
2695+ if (gtk_tree_model_get_iter (model, &iter, (GtkTreePath *) l->data)) {
2696+ gchar *name, *email;
2697+ GdkPixbuf *icon;
2698 SelectedContactInfo *sci;
2699
2700- markup = g_strdup_printf ("<b><big>%s</big></b>", name);
2701- gtk_list_store_set (GTK_LIST_STORE (model), &iter,
2702- CONTACTS_VIEW_COLUMN_MARKUP, markup,
2703+ gtk_tree_model_get (model, &iter,
2704+ CONTACTS_VIEW_COLUMN_NAME, &name,
2705+ CONTACTS_VIEW_COLUMN_EMAIL, &email,
2706+ CONTACTS_VIEW_COLUMN_PIXBUF, &icon,
2707 -1);
2708- g_free (markup);
2709
2710 sci = g_new0 (SelectedContactInfo, 1);
2711 sci->name = g_strdup (name);
2712@@ -159,6 +164,12 @@
2713 save_recently_used_list (cv);
2714 }
2715 }
2716+
2717+ /* Free memory */
2718+ g_list_foreach (selected_items, (GFunc) gtk_tree_path_free, NULL);
2719+ g_list_free (selected_items);
2720+
2721+ g_signal_emit_by_name (cv, "selection-changed", NULL);
2722 }
2723
2724 static void
2725@@ -258,7 +269,7 @@
2726 }
2727
2728 static void
2729-add_contacts (ContactsView *cv, GList *contacts)
2730+add_contacts (ContactsView *cv, GList *contacts, GHashTable *selection_hash)
2731 {
2732 GList *l;
2733
2734@@ -266,7 +277,7 @@
2735 EContact *contact = l->data;
2736
2737 /* We add the selected items when searching, so ignore them here */
2738- if (g_hash_table_lookup (cv->selection, (gconstpointer) e_contact_get_const (contact, E_CONTACT_FULL_NAME)))
2739+ if (g_hash_table_lookup (selection_hash, (gconstpointer) e_contact_get_const (contact, E_CONTACT_FULL_NAME)))
2740 continue;
2741
2742 add_one_contact (cv,
2743@@ -290,15 +301,19 @@
2744
2745 if (status != E_BOOK_ERROR_OK) {
2746 g_warning ("Error opening addressbook: %d", status);
2747+ g_object_unref (G_OBJECT (book));
2748 return;
2749 }
2750
2751+ /* Add the book to the list of opened books */
2752+ cv->books = g_slist_append (cv->books, book);
2753+
2754 /* Get all contacts for this book */
2755 query = e_book_query_any_field_contains ("");
2756 e_book_get_contacts (book, query, &contacts, NULL);
2757 e_book_query_unref (query);
2758
2759- add_contacts (cv, contacts);
2760+ add_contacts (cv, contacts, cv->selection);
2761
2762 }
2763
2764@@ -352,8 +367,8 @@
2765 gtk_widget_show (scroll);
2766
2767 cv->recently_used_view = gtk_icon_view_new ();
2768- g_signal_connect (G_OBJECT (cv->recently_used_view), "item-activated",
2769- G_CALLBACK (item_activated_cb), cv);
2770+ g_signal_connect (G_OBJECT (cv->recently_used_view), "selection-changed",
2771+ G_CALLBACK (selection_changed_cb), cv);
2772 gtk_icon_view_set_text_column (GTK_ICON_VIEW (cv->recently_used_view), CONTACTS_VIEW_COLUMN_NAME);
2773 gtk_icon_view_set_markup_column (GTK_ICON_VIEW (cv->recently_used_view), CONTACTS_VIEW_COLUMN_MARKUP);
2774 gtk_icon_view_set_pixbuf_column (GTK_ICON_VIEW (cv->recently_used_view), CONTACTS_VIEW_COLUMN_PIXBUF);
2775@@ -374,8 +389,8 @@
2776 gtk_widget_show (scroll);
2777
2778 cv->alphabetical_view = gtk_icon_view_new ();
2779- g_signal_connect (G_OBJECT (cv->alphabetical_view), "item-activated",
2780- G_CALLBACK (item_activated_cb), cv);
2781+ g_signal_connect (G_OBJECT (cv->alphabetical_view), "selection-changed",
2782+ G_CALLBACK (selection_changed_cb), cv);
2783 gtk_icon_view_set_text_column (GTK_ICON_VIEW (cv->alphabetical_view), CONTACTS_VIEW_COLUMN_NAME);
2784 gtk_icon_view_set_markup_column (GTK_ICON_VIEW (cv->alphabetical_view), CONTACTS_VIEW_COLUMN_MARKUP);
2785 gtk_icon_view_set_pixbuf_column (GTK_ICON_VIEW (cv->alphabetical_view), CONTACTS_VIEW_COLUMN_PIXBUF);
2786@@ -410,7 +425,6 @@
2787 book = e_book_new ((ESource *) sl->data, &error);
2788 if (book != NULL) {
2789 e_book_async_open (book, TRUE, (EBookCallback) book_opened_cb, cv);
2790- cv->books = g_slist_append (cv->books, book);
2791 } else {
2792 g_warning ("Could not open addressbook %s: %s", e_source_get_uri (sl->data), error->message);
2793 g_error_free (error);
2794@@ -426,39 +440,57 @@
2795 }
2796
2797 static void
2798-append_selected_to_model (GtkListStore *model, SelectedContactInfo *sli)
2799+append_selected_to_model (GtkIconView *icon_view, SelectedContactInfo *sci)
2800 {
2801- gchar *markup;
2802 GtkTreeIter new_row;
2803-
2804- markup = g_strdup_printf ("<b><big>%s</big></b>", (const gchar *) sli->name);
2805+ GtkListStore *model = GTK_LIST_STORE (gtk_icon_view_get_model (icon_view));
2806
2807 gtk_list_store_prepend (model, &new_row);
2808 gtk_list_store_set (GTK_LIST_STORE (model), &new_row,
2809- CONTACTS_VIEW_COLUMN_NAME, sli->name,
2810- CONTACTS_VIEW_COLUMN_MARKUP, markup,
2811- CONTACTS_VIEW_COLUMN_EMAIL, sli->email,
2812- CONTACTS_VIEW_COLUMN_PIXBUF, sli->pixbuf,
2813+ CONTACTS_VIEW_COLUMN_NAME, sci->name,
2814+ CONTACTS_VIEW_COLUMN_MARKUP, sci->name,
2815+ CONTACTS_VIEW_COLUMN_EMAIL, sci->email,
2816+ CONTACTS_VIEW_COLUMN_PIXBUF, sci->pixbuf,
2817 -1);
2818
2819- g_free (markup);
2820+ gtk_icon_view_select_path (icon_view,
2821+ gtk_tree_model_get_path (GTK_TREE_MODEL (model), &new_row));
2822 }
2823
2824 static void
2825 foreach_selected_to_model_cb (gpointer key, gpointer value, gpointer user_data)
2826 {
2827- GtkListStore *model;
2828- SelectedContactInfo *sli = (SelectedContactInfo *) value;
2829+ SelectedContactInfo *sci = (SelectedContactInfo *) value;
2830 ContactsView *cv = CONTACTS_VIEW (user_data);
2831
2832- append_selected_to_model (GTK_LIST_STORE (gtk_icon_view_get_model (GTK_ICON_VIEW (cv->recently_used_view))), sli);
2833- append_selected_to_model (GTK_LIST_STORE (gtk_icon_view_get_model (GTK_ICON_VIEW (cv->alphabetical_view))), sli);
2834+ append_selected_to_model (GTK_ICON_VIEW (cv->recently_used_view), sci);
2835+ append_selected_to_model (GTK_ICON_VIEW (cv->alphabetical_view), sci);
2836 }
2837
2838 void
2839 contacts_view_search (ContactsView *cv, const gchar *search_string)
2840 {
2841 GSList *l;
2842+ GHashTable *tmp_selection;
2843+ GHashTableIter hash_iter;
2844+ gpointer key, value;
2845+
2846+ /* Make a copy of the selected items before changing the models */
2847+ tmp_selection = g_hash_table_new_full (g_str_hash, g_str_equal,
2848+ (GDestroyNotify) g_free,
2849+ (GDestroyNotify) free_selected_contact_info);
2850+ g_hash_table_iter_init (&hash_iter, cv->selection);
2851+ while (g_hash_table_iter_next (&hash_iter, &key, &value)) {
2852+ SelectedContactInfo *new_sci, *old_sci;
2853+
2854+ old_sci = (SelectedContactInfo *) value;
2855+
2856+ new_sci = g_new0 (SelectedContactInfo, 1);
2857+ new_sci->name = g_strdup (old_sci->name);
2858+ new_sci->email = g_strdup (old_sci->email);
2859+ new_sci->pixbuf = gdk_pixbuf_ref (old_sci->pixbuf);
2860+ g_hash_table_insert (tmp_selection, g_strdup (old_sci->name), new_sci);
2861+ }
2862
2863 /* Reset the icon views */
2864 gtk_list_store_clear (GTK_LIST_STORE (gtk_icon_view_get_model (GTK_ICON_VIEW (cv->recently_used_view))));
2865@@ -481,11 +513,12 @@
2866 e_book_get_contacts (book, query, &contacts, NULL);
2867 e_book_query_unref (query);
2868
2869- add_contacts (cv, contacts);
2870+ add_contacts (cv, contacts, tmp_selection);
2871 }
2872
2873 /* Now add selected contacts */
2874- g_hash_table_foreach (cv->selection, (GHFunc) foreach_selected_to_model_cb, cv);
2875+ g_hash_table_foreach (tmp_selection, (GHFunc) foreach_selected_to_model_cb, cv);
2876+ g_hash_table_destroy (tmp_selection);
2877 }
2878
2879 static void
2880@@ -505,3 +538,59 @@
2881 g_hash_table_foreach (cv->selection, (GHFunc) add_selection_to_list_cb, &selection);
2882 return selection;
2883 }
2884+
2885+guint
2886+contacts_view_get_contacts_count (ContactsView *cv)
2887+{
2888+ return gtk_tree_model_iter_n_children (gtk_icon_view_get_model (GTK_ICON_VIEW (cv->alphabetical_view)),
2889+ NULL);
2890+}
2891+
2892+void
2893+contacts_view_add_contact (ContactsView *cv, const gchar *contact_name, const gchar *contact_email)
2894+{
2895+ SelectedContactInfo *sci;
2896+ GtkIconTheme *icon_theme;
2897+ GSList *l;
2898+
2899+ icon_theme = gtk_icon_theme_get_default ();
2900+
2901+ /* First add the new contact to the list of selected ones */
2902+ sci = g_new0 (SelectedContactInfo, 1);
2903+ sci->name = g_strdup (contact_name);
2904+ sci->email = g_strdup (contact_email);
2905+ sci->pixbuf = gtk_icon_theme_load_icon (icon_theme, GTK_STOCK_ORIENTATION_PORTRAIT, 64, 0, NULL);
2906+ g_hash_table_insert (cv->selection, g_strdup (contact_name), sci);
2907+
2908+ /* Add it to the recently used list */
2909+ g_hash_table_insert (cv->recently_used, g_strdup (sci->name), sci->name);
2910+ save_recently_used_list (cv);
2911+
2912+ /* And now add it to the icon views */
2913+ append_selected_to_model (GTK_ICON_VIEW (cv->recently_used_view), sci);
2914+ append_selected_to_model (GTK_ICON_VIEW (cv->alphabetical_view), sci);
2915+
2916+ /* Add the contact to the CouchDB addressbook, if possible */
2917+ for (l = cv->books; l != NULL; l = l->next) {
2918+ const gchar *uri;
2919+
2920+ uri = e_book_get_uri (E_BOOK (l->data));
2921+ if (g_str_has_prefix (uri, "couchdb://127.0.0.1")) {
2922+ EContact *contact;
2923+ GError *error = NULL;
2924+
2925+ contact = e_contact_new ();
2926+ e_contact_set (contact, E_CONTACT_FULL_NAME, (gconstpointer) contact_name);
2927+ e_contact_set (contact, E_CONTACT_EMAIL_1, (gconstpointer) contact_email);
2928+
2929+ if (!e_book_add_contact (E_BOOK (l->data), contact, &error)) {
2930+ g_warning ("Could not add contact to %s: %s", uri, error->message);
2931+ g_error_free (error);
2932+ }
2933+
2934+ g_object_unref (G_OBJECT (contact));
2935+
2936+ break;
2937+ }
2938+ }
2939+}
2940
2941=== modified file 'nautilus/contacts-view.h'
2942--- nautilus/contacts-view.h 2010-02-17 23:51:29 +0000
2943+++ nautilus/contacts-view.h 2010-03-31 23:55:45 +0000
2944@@ -54,6 +54,7 @@
2945 GtkNotebookClass parent_class;
2946
2947 /* Signals */
2948+ void (* selection_changed) (ContactsView *cv);
2949 void (* contacts_count_changed) (ContactsView *cv, gint total);
2950 } ContactsViewClass;
2951
2952@@ -62,5 +63,7 @@
2953 GtkWidget *contacts_view_new (void);
2954 void contacts_view_search (ContactsView *cv, const gchar *search_string);
2955 GSList *contacts_view_get_selected_emails (ContactsView *cv);
2956+guint contacts_view_get_contacts_count (ContactsView *cv);
2957+void contacts_view_add_contact (ContactsView *cv, const gchar *contact_name, const gchar *contact_email);
2958
2959 #endif
2960
2961=== modified file 'nautilus/u1-contacts-picker.c'
2962--- nautilus/u1-contacts-picker.c 2010-02-17 23:51:29 +0000
2963+++ nautilus/u1-contacts-picker.c 2010-03-31 23:55:45 +0000
2964@@ -27,7 +27,17 @@
2965 GtkWidget *search_entry;
2966 GtkWidget *total_label;
2967 GtkWidget *contacts_view;
2968-};
2969+
2970+ /* Hidden widgets to add a new contact */
2971+ GtkWidget *add_contact_button;
2972+};
2973+
2974+enum {
2975+ SELECTION_CHANGED_SIGNAL,
2976+ LAST_SIGNAL
2977+};
2978+
2979+static guint u1_contacts_picker_signals[LAST_SIGNAL] = { 0, };
2980
2981 G_DEFINE_TYPE(U1ContactsPicker, u1_contacts_picker, GTK_TYPE_VBOX)
2982
2983@@ -50,14 +60,42 @@
2984 GObjectClass *object_class = G_OBJECT_CLASS (klass);
2985
2986 object_class->finalize = u1_contacts_picker_finalize;
2987+
2988+ /* Register object signals */
2989+ u1_contacts_picker_signals[SELECTION_CHANGED_SIGNAL] =
2990+ g_signal_new ("selection-changed",
2991+ G_TYPE_FROM_CLASS (klass),
2992+ (GSignalFlags) G_SIGNAL_RUN_LAST,
2993+ G_STRUCT_OFFSET (U1ContactsPickerClass, selection_changed),
2994+ NULL,
2995+ NULL,
2996+ g_cclosure_marshal_VOID__VOID,
2997+ G_TYPE_NONE,
2998+ 0);
2999 }
3000
3001 static void
3002-search_activated_cb (GtkEntry *entry, gpointer data)
3003+search_activated_cb (GtkEditable *entry, gpointer data)
3004 {
3005+ const gchar *text;
3006 U1ContactsPicker *picker = (U1ContactsPicker *) data;
3007
3008- contacts_view_search (CONTACTS_VIEW (picker->priv->contacts_view), gtk_entry_get_text (entry));
3009+ text = gtk_entry_get_text (GTK_ENTRY (entry));
3010+ contacts_view_search (CONTACTS_VIEW (picker->priv->contacts_view), text);
3011+
3012+ /* If no contacts offer the user to add it to the contacts database */
3013+ if (contacts_view_get_contacts_count (CONTACTS_VIEW (picker->priv->contacts_view)) == 0)
3014+ gtk_widget_show (picker->priv->add_contact_button);
3015+ else
3016+ gtk_widget_hide (picker->priv->add_contact_button);
3017+}
3018+
3019+static void
3020+view_selection_changed_cb (ContactsView *cv, gpointer user_data)
3021+{
3022+ U1ContactsPicker *picker = U1_CONTACTS_PICKER (user_data);
3023+
3024+ g_signal_emit (picker, u1_contacts_picker_signals[SELECTION_CHANGED_SIGNAL], 0);
3025 }
3026
3027 static void
3028@@ -66,41 +104,102 @@
3029 gchar *label;
3030 U1ContactsPicker *picker = U1_CONTACTS_PICKER (user_data);
3031
3032- label = g_strdup_printf (ngettext ("%d contact", "%d contacts", total), total);
3033+ if (strlen (gtk_entry_get_text (GTK_ENTRY (picker->priv->search_entry))) > 0)
3034+ label = g_strdup_printf (ngettext ("Found %d match", "Found %d matches", total), total);
3035+ else
3036+ label = g_strdup_printf (ngettext ("%d contact", "%d contacts", total), total);
3037 gtk_label_set_text (GTK_LABEL (picker->priv->total_label), label);
3038
3039 g_free (label);
3040 }
3041
3042 static void
3043+add_contact_cb (GtkButton *button, gpointer user_data)
3044+{
3045+ GtkWidget *dialog, *table, *label, *name_entry, *email_entry;
3046+ const gchar *search_text;
3047+ U1ContactsPicker *picker = (U1ContactsPicker *) user_data;
3048+
3049+ /* Build the dialog */
3050+ dialog = gtk_dialog_new_with_buttons (_("Add contact"),
3051+ GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (picker))),
3052+ GTK_DIALOG_NO_SEPARATOR,
3053+ GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
3054+ GTK_STOCK_ADD, GTK_RESPONSE_OK,
3055+ NULL);
3056+ table = gtk_table_new (2, 2, FALSE);
3057+ gtk_widget_show (table);
3058+ gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
3059+ table, TRUE, TRUE, 3);
3060+
3061+ label = gtk_label_new (_("Contact name"));
3062+ gtk_widget_show (label);
3063+ gtk_table_attach (GTK_TABLE (table), label, 0, 1, 0, 1, GTK_FILL, GTK_FILL, 3, 3);
3064+ name_entry = gtk_entry_new ();
3065+ gtk_widget_show (name_entry);
3066+ gtk_table_attach (GTK_TABLE (table), name_entry, 1, 2, 0, 1, GTK_FILL, GTK_FILL, 3, 3);
3067+
3068+ label = gtk_label_new (_("Email address"));
3069+ gtk_widget_show (label);
3070+ gtk_table_attach (GTK_TABLE (table), label, 0, 1, 1, 2, GTK_FILL, GTK_FILL, 3, 3);
3071+ email_entry = gtk_entry_new ();
3072+ gtk_widget_show (email_entry);
3073+ gtk_table_attach (GTK_TABLE (table), email_entry, 1, 2, 1, 2, GTK_FILL, GTK_FILL, 3, 3);
3074+
3075+ search_text = gtk_entry_get_text (GTK_ENTRY (picker->priv->search_entry));
3076+ if (g_strrstr (search_text, "@") != NULL)
3077+ gtk_entry_set_text (GTK_ENTRY (email_entry), search_text);
3078+ else
3079+ gtk_entry_set_text (GTK_ENTRY (name_entry), search_text);
3080+
3081+ /* Run the dialog */
3082+ gtk_widget_grab_focus (name_entry);
3083+ if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_OK) {
3084+ contacts_view_add_contact (CONTACTS_VIEW (picker->priv->contacts_view),
3085+ gtk_entry_get_text (GTK_ENTRY (name_entry)),
3086+ gtk_entry_get_text (GTK_ENTRY (email_entry)));
3087+ gtk_entry_set_text (GTK_ENTRY (picker->priv->search_entry), "");
3088+ }
3089+
3090+ gtk_widget_destroy (dialog);
3091+ gtk_widget_hide (picker->priv->add_contact_button);
3092+}
3093+
3094+static void
3095 u1_contacts_picker_init (U1ContactsPicker *picker)
3096 {
3097- GtkWidget *table, *scroll;
3098- GtkTreeModel *model;
3099+ GtkWidget *table;
3100
3101 picker->priv = g_new0 (U1ContactsPickerPrivate, 1);
3102
3103 /* Create the table to contain the layout */
3104- table = gtk_table_new (3, 2, FALSE);
3105+ table = gtk_table_new (4, 3, FALSE);
3106 gtk_widget_show (table);
3107 gtk_box_pack_start (GTK_BOX (picker), table, TRUE, TRUE, 3);
3108
3109 /* Create the search area */
3110 picker->priv->search_entry = gtk_entry_new ();
3111- g_signal_connect (G_OBJECT (picker->priv->search_entry), "activate", G_CALLBACK (search_activated_cb), picker);
3112+ g_signal_connect (G_OBJECT (picker->priv->search_entry), "changed", G_CALLBACK (search_activated_cb), picker);
3113 gtk_widget_show (picker->priv->search_entry);
3114 gtk_table_attach (GTK_TABLE (table), picker->priv->search_entry, 0, 1, 0, 1, GTK_FILL, GTK_FILL, 3, 3);
3115
3116- picker->priv->total_label = gtk_label_new (ngettext ("%d contact", "%d contacts", 0));
3117+ picker->priv->add_contact_button = gtk_button_new_from_stock (GTK_STOCK_ADD);
3118+ g_signal_connect (G_OBJECT (picker->priv->add_contact_button), "clicked",
3119+ G_CALLBACK (add_contact_cb), picker);
3120+ gtk_table_attach (GTK_TABLE (table), picker->priv->add_contact_button, 1, 2, 0, 1, GTK_FILL, GTK_FILL, 3, 3);
3121+
3122+ picker->priv->total_label = gtk_label_new (ngettext ("0 contact", "0 contacts", 0));
3123 gtk_widget_show (picker->priv->total_label);
3124- gtk_table_attach (GTK_TABLE (table), picker->priv->total_label, 1, 2, 0, 1, GTK_FILL, GTK_FILL, 3, 3);
3125-
3126+ gtk_table_attach (GTK_TABLE (table), picker->priv->total_label, 2, 3, 0, 1, GTK_FILL, GTK_FILL, 3, 3);
3127+
3128 /* Create the contacts view */
3129 picker->priv->contacts_view = contacts_view_new ();
3130+ g_signal_connect (G_OBJECT (picker->priv->contacts_view), "selection-changed",
3131+ G_CALLBACK (view_selection_changed_cb), picker);
3132 g_signal_connect (G_OBJECT (picker->priv->contacts_view), "contacts-count-changed",
3133 G_CALLBACK (contacts_count_changed_cb), picker);
3134 gtk_widget_show (picker->priv->contacts_view);
3135- gtk_table_attach (GTK_TABLE (table), picker->priv->contacts_view, 0, 2, 1, 3,
3136+ gtk_table_attach (GTK_TABLE (table), picker->priv->contacts_view, 0, 3, 2, 4,
3137 GTK_FILL | GTK_EXPAND | GTK_SHRINK,
3138 GTK_FILL | GTK_EXPAND | GTK_SHRINK,
3139 3, 3);
3140
3141=== modified file 'nautilus/u1-contacts-picker.h'
3142--- nautilus/u1-contacts-picker.h 2010-02-17 23:51:29 +0000
3143+++ nautilus/u1-contacts-picker.h 2010-03-31 23:55:45 +0000
3144@@ -44,6 +44,9 @@
3145
3146 struct _U1ContactsPickerClass {
3147 GtkVBoxClass parent_class;
3148+
3149+ /* Signals */
3150+ void (* selection_changed) (U1ContactsPicker *picker);
3151 };
3152
3153 GType u1_contacts_picker_get_type (void);
3154
3155=== modified file 'nautilus/ubuntuone-nautilus.c'
3156--- nautilus/ubuntuone-nautilus.c 2010-03-10 23:36:53 +0000
3157+++ nautilus/ubuntuone-nautilus.c 2010-03-31 23:55:45 +0000
3158@@ -328,7 +328,7 @@
3159 const char * uri,
3160 GtkWidget * parent) {
3161 UbuntuOneNautilus * uon;
3162- gchar * path, * upath;
3163+ gchar * path;
3164 GtkWidget * hbox = NULL;
3165 GtkWidget * label;
3166 gchar * labeltext;
3167@@ -402,6 +402,20 @@
3168 data = NULL;
3169 }
3170
3171+static void
3172+picker_selection_changed_cb (U1ContactsPicker *picker, gpointer user_data)
3173+{
3174+ GSList *selection;
3175+ GtkWidget * dialog = (GtkWidget *) user_data;
3176+
3177+ selection = u1_contacts_picker_get_selected_emails (picker);
3178+ if (selection != NULL) {
3179+ gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT, TRUE);
3180+ u1_contacts_picker_free_selection_list (selection);
3181+ } else
3182+ gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT, FALSE);
3183+}
3184+
3185 /* Share on Ubuntu One dialog constructor */
3186 static GtkWidget * ubuntuone_nautilus_share_dialog_construct (struct _CBData * data) {
3187 GtkWidget * dialog;
3188@@ -414,10 +428,11 @@
3189 gtk_dialog_set_has_separator (GTK_DIALOG (dialog), FALSE);
3190 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
3191 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
3192- ("Share"), GTK_RESPONSE_ACCEPT,
3193+ (_("Share")), GTK_RESPONSE_ACCEPT,
3194 NULL);
3195 gtk_dialog_set_default_response (GTK_DIALOG (dialog),
3196 GTK_RESPONSE_ACCEPT);
3197+ gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT, FALSE);
3198 gtk_window_set_icon_name (GTK_WINDOW (dialog), "ubuntuone");
3199
3200 area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
3201@@ -436,6 +451,8 @@
3202 gtk_widget_show (label);
3203
3204 data->user_picker = u1_contacts_picker_new ();
3205+ g_signal_connect (G_OBJECT (data->user_picker), "selection-changed",
3206+ G_CALLBACK (picker_selection_changed_cb), dialog);
3207 gtk_table_attach (GTK_TABLE (table), data->user_picker, 0, 2, 1, 2,
3208 GTK_FILL | GTK_EXPAND | GTK_SHRINK,
3209 GTK_FILL | GTK_EXPAND | GTK_SHRINK, 3, 3);
3210
3211=== modified file 'tests/__init__.pyc'
3212Binary files tests/__init__.pyc 2010-03-10 23:36:53 +0000 and tests/__init__.pyc 2010-03-31 23:55:45 +0000 differ
3213=== modified file 'tests/oauthdesktop/__init__.pyc'
3214Binary files tests/oauthdesktop/__init__.pyc 2010-03-10 23:36:53 +0000 and tests/oauthdesktop/__init__.pyc 2010-03-31 23:55:45 +0000 differ
3215=== modified file 'tests/oauthdesktop/test_auth.pyc'
3216Binary files tests/oauthdesktop/test_auth.pyc 2010-03-10 23:36:53 +0000 and tests/oauthdesktop/test_auth.pyc 2010-03-31 23:55:45 +0000 differ
3217=== modified file 'tests/oauthdesktop/test_config.pyc'
3218Binary files tests/oauthdesktop/test_config.pyc 2010-03-10 23:36:53 +0000 and tests/oauthdesktop/test_config.pyc 2010-03-31 23:55:45 +0000 differ
3219=== modified file 'tests/oauthdesktop/test_key_acls.pyc'
3220Binary files tests/oauthdesktop/test_key_acls.pyc 2010-03-10 23:36:53 +0000 and tests/oauthdesktop/test_key_acls.pyc 2010-03-31 23:55:45 +0000 differ
3221=== modified file 'tests/oauthdesktop/test_main.pyc'
3222Binary files tests/oauthdesktop/test_main.pyc 2010-03-10 23:36:53 +0000 and tests/oauthdesktop/test_main.pyc 2010-03-31 23:55:45 +0000 differ
3223=== modified file 'tests/syncdaemon/__init__.pyc'
3224Binary files tests/syncdaemon/__init__.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/__init__.pyc 2010-03-31 23:55:45 +0000 differ
3225=== modified file 'tests/syncdaemon/fsm/__init__.pyc'
3226Binary files tests/syncdaemon/fsm/__init__.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/fsm/__init__.pyc 2010-03-31 23:55:45 +0000 differ
3227=== modified file 'tests/syncdaemon/fsm/test_fsm.pyc'
3228Binary files tests/syncdaemon/fsm/test_fsm.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/fsm/test_fsm.pyc 2010-03-31 23:55:45 +0000 differ
3229=== modified file 'tests/syncdaemon/fsm/test_fsm_run.pyc'
3230Binary files tests/syncdaemon/fsm/test_fsm_run.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/fsm/test_fsm_run.pyc 2010-03-31 23:55:45 +0000 differ
3231=== modified file 'tests/syncdaemon/fsm/test_run_hello.pyc'
3232Binary files tests/syncdaemon/fsm/test_run_hello.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/fsm/test_run_hello.pyc 2010-03-31 23:55:45 +0000 differ
3233=== modified file 'tests/syncdaemon/test_action_predicates.pyc'
3234Binary files tests/syncdaemon/test_action_predicates.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_action_predicates.pyc 2010-03-31 23:55:45 +0000 differ
3235=== modified file 'tests/syncdaemon/test_action_queue.py'
3236--- tests/syncdaemon/test_action_queue.py 2010-03-10 23:36:53 +0000
3237+++ tests/syncdaemon/test_action_queue.py 2010-03-31 23:55:45 +0000
3238@@ -47,7 +47,7 @@
3239 from ubuntuone.syncdaemon.action_queue import (
3240 ActionQueue, ActionQueueCommand, ChangePublicAccess, CreateUDF,
3241 DeleteVolume, ListDir, ListVolumes, NoisyRequestQueue, RequestQueue,
3242- Upload
3243+ Upload, CreateShare,
3244 )
3245 from ubuntuone.syncdaemon.event_queue import EventQueue, EVENTS
3246 from ubuntuone.syncdaemon.volume_manager import UDF
3247@@ -1183,6 +1183,65 @@
3248 self.assertEquals(events, self.command.action_queue.event_queue.events)
3249
3250
3251+class CreateShareTestCase(ConnectedBaseTestCase):
3252+ """Test for CreateShare ActionQueueCommand."""
3253+
3254+ @defer.inlineCallbacks
3255+ def setUp(self):
3256+ """Init."""
3257+ yield super(CreateShareTestCase, self).setUp()
3258+ self.request_queue = RequestQueue(name='foo', action_queue=self.action_queue)
3259+ self.orig_create_share_http = CreateShare._create_share_http
3260+
3261+ @defer.inlineCallbacks
3262+ def tearDown(self):
3263+ yield super(CreateShareTestCase, self).tearDown()
3264+ CreateShare._create_share_http = self.orig_create_share_http
3265+
3266+ @defer.inlineCallbacks
3267+ def test_access_level_modify_http(self):
3268+ """Test proper handling of the access level in the http case."""
3269+ # replace _create_share_http with a fake, just to check the args
3270+ d = defer.Deferred()
3271+ def check_create_http(self, node_id, user, name, read_only, deferred):
3272+ """Fire the deferred with the args."""
3273+ d.callback((node_id, user, name, read_only))
3274+ deferred.callback(None)
3275+ CreateShare._create_share_http = check_create_http
3276+ command = CreateShare(self.request_queue, 'node_id',
3277+ 'share_to@example.com', 'share_name',
3278+ 'Modify', 'marker')
3279+ self.assertTrue(command.use_http, 'CreateShare should be in http mode')
3280+
3281+ command._run()
3282+ node_id, user, name, read_only = yield d
3283+ self.assertEquals('node_id', node_id)
3284+ self.assertEquals('share_to@example.com', user)
3285+ self.assertEquals('share_name', name)
3286+ self.assertFalse(read_only)
3287+
3288+ @defer.inlineCallbacks
3289+ def test_access_level_view_http(self):
3290+ """Test proper handling of the access level in the http case."""
3291+ # replace _create_share_http with a fake, just to check the args
3292+ d = defer.Deferred()
3293+ def check_create_http(self, node_id, user, name, read_only, deferred):
3294+ """Fire the deferred with the args."""
3295+ d.callback((node_id, user, name, read_only))
3296+ deferred.callback(None)
3297+ CreateShare._create_share_http = check_create_http
3298+ command = CreateShare(self.request_queue, 'node_id',
3299+ 'share_to@example.com', 'share_name',
3300+ 'View', 'marker')
3301+ self.assertTrue(command.use_http, 'CreateShare should be in http mode')
3302+ command._run()
3303+ node_id, user, name, read_only = yield d
3304+ self.assertEquals('node_id', node_id)
3305+ self.assertEquals('share_to@example.com', user)
3306+ self.assertEquals('share_name', name)
3307+ self.assertTrue(read_only)
3308+
3309+
3310 class RequestQueueManager(FactoryBaseTestCase):
3311 """Test how RequestQueue manages the queues."""
3312
3313
3314=== modified file 'tests/syncdaemon/test_action_queue.pyc'
3315Binary files tests/syncdaemon/test_action_queue.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_action_queue.pyc 2010-03-31 23:55:45 +0000 differ
3316=== modified file 'tests/syncdaemon/test_config.py'
3317--- tests/syncdaemon/test_config.py 2010-03-04 16:47:43 +0000
3318+++ tests/syncdaemon/test_config.py 2010-03-31 23:55:45 +0000
3319@@ -395,6 +395,21 @@
3320 conf.read(config.get_user_config().config_file)
3321 self.assertEquals(conf.get(config.THROTTLING, 'on'), 'False')
3322
3323+ def test_files_sync_enabled(self):
3324+ """Test for toggling files sync."""
3325+ dbus_config = self.main.dbus_iface.config
3326+ enabled = dbus_config.files_sync_enabled()
3327+ self.assertTrue(enabled)
3328+ dbus_config.set_files_sync_enabled(False)
3329+ enabled = dbus_config.files_sync_enabled()
3330+ self.assertFalse(enabled)
3331+ dbus_config.set_files_sync_enabled(True)
3332+ enabled = dbus_config.files_sync_enabled()
3333+ self.assertTrue(enabled)
3334+ conf = ConfigParser()
3335+ conf.read(config.get_user_config().config_file)
3336+ self.assertEquals(conf.get(config.MAIN, 'files_sync_enabled'), 'True')
3337+
3338
3339 class SyncDaemonConfigParserTests(BaseTwistedTestCase):
3340 """Tests for SyncDaemonConfigParser"""
3341@@ -474,3 +489,27 @@
3342 self.assertRaises(ValueError, self.cp.add_upgrade_hook, 'foo', 'bar',
3343 lambda x: None)
3344
3345+ def test_ignore_one(self):
3346+ """Test ignore files config, one regex."""
3347+ conf_file = os.path.join(self.test_root, 'test_new_config.conf')
3348+ with open(conf_file, 'w') as fp:
3349+ fp.write('[__main__]\n')
3350+ fp.write('ignore = .*\\.pyc\n') # all .pyc files
3351+ self.assertTrue(os.path.exists(conf_file))
3352+ self.cp.read([conf_file])
3353+ self.cp.parse_all()
3354+ self.assertEquals(self.cp.get('__main__', 'ignore').value,
3355+ [r'.*\.pyc'])
3356+
3357+ def test_ignore_two(self):
3358+ """Test ignore files config, two regexes."""
3359+ conf_file = os.path.join(self.test_root, 'test_new_config.conf')
3360+ with open(conf_file, 'w') as fp:
3361+ fp.write('[__main__]\n')
3362+ fp.write('ignore = .*\\.pyc\n') # all .pyc files
3363+ fp.write(' .*\\.sw[opnx]\n') # all gvim temp files
3364+ self.assertTrue(os.path.exists(conf_file))
3365+ self.cp.read([conf_file])
3366+ self.cp.parse_all()
3367+ self.assertEquals(self.cp.get('__main__', 'ignore').value,
3368+ ['.*\\.pyc', '.*\\.sw[opnx]'])
3369
3370=== modified file 'tests/syncdaemon/test_config.pyc'
3371Binary files tests/syncdaemon/test_config.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_config.pyc 2010-03-31 23:55:45 +0000 differ
3372=== modified file 'tests/syncdaemon/test_dbus.py'
3373--- tests/syncdaemon/test_dbus.py 2010-03-10 23:36:53 +0000
3374+++ tests/syncdaemon/test_dbus.py 2010-03-31 23:55:45 +0000
3375@@ -1133,6 +1133,23 @@
3376 self.event_q.push(event_name, *args)
3377 return d
3378
3379+ def test_invalid_filename(self):
3380+ """Test the FS_INVALID_NAME signal."""
3381+ d = defer.Deferred()
3382+
3383+ def handler(dirname, filename):
3384+ """Handler for InvalidName signal."""
3385+ self.assertTrue(isinstance(dirname, unicode))
3386+ self.assertTrue(isinstance(filename, str))
3387+ d.callback(True)
3388+
3389+ match = self.bus.add_signal_receiver(handler,
3390+ signal_name='InvalidName',
3391+ byte_arrays=True)
3392+ self.signal_receivers.add(match)
3393+ self.main.event_q.push('FS_INVALID_NAME', u'test/dir', 'testpath')
3394+ return d
3395+
3396 def test_share_changed(self):
3397 """ Test the ShareChanged signal. """
3398 share_path = os.path.join(self.main.shares_dir, 'share')
3399
3400=== modified file 'tests/syncdaemon/test_dbus.pyc'
3401Binary files tests/syncdaemon/test_dbus.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_dbus.pyc 2010-03-31 23:55:45 +0000 differ
3402=== modified file 'tests/syncdaemon/test_eq_inotify.py'
3403--- tests/syncdaemon/test_eq_inotify.py 2010-03-04 16:47:43 +0000
3404+++ tests/syncdaemon/test_eq_inotify.py 2010-03-31 23:55:45 +0000
3405@@ -31,6 +31,18 @@
3406 from tests.syncdaemon.test_eventqueue import BaseEQTestCase
3407
3408
3409+class DontHitMe(object):
3410+ """We shouldn't be called."""
3411+
3412+ def __init__(self, test_instance):
3413+ self.test_instance = test_instance
3414+
3415+ def handle_default(self, *a, **k):
3416+ """Something here? Error!"""
3417+ self.test_inst.finished_error("Don't hit me! Received %s %s" % (a, k))
3418+
3419+
3420+
3421 class WatchTests(BaseEQTestCase):
3422 """Test the EQ API to add and remove watchs."""
3423
3424@@ -393,20 +405,13 @@
3425 open(fromfile, "w").close()
3426 symlpath = os.path.join(testdir, "syml")
3427
3428- class DontHitMe(object):
3429- """we shouldn't be called"""
3430- # class-closure, cannot use self, pylint: disable-msg=E0213
3431- def handle_default(innerself, *a):
3432- """Something here? Error!"""
3433- self.finished_error("don't hit me! received %s" % (a,))
3434-
3435 def confirm():
3436 """check result."""
3437 self.finished_ok()
3438
3439 # set up everything and freeze
3440 self.eq.inotify_add_watch(testdir)
3441- self.eq.subscribe(DontHitMe())
3442+ self.eq.subscribe(DontHitMe(self))
3443
3444 os.symlink(fromfile, symlpath)
3445 reactor.callLater(.1, confirm)
3446@@ -1026,13 +1031,6 @@
3447 testfile = os.path.join(testdir, "bar")
3448 os.mkdir(testdir)
3449
3450- class DontHitMe(object):
3451- """we shouldn't be called"""
3452- # class-closure, cannot use self, pylint: disable-msg=E0213
3453- def handle_default(innerself, *a):
3454- """Something here? Error!"""
3455- self.finished_error("don't hit me! received %s" % (a,))
3456-
3457 def freeze_commit():
3458 """release and check result."""
3459 d = self.eq.freeze_commit([("FS_DIR_DELETE", "foobar")])
3460@@ -1046,7 +1044,7 @@
3461
3462 # set up everything and freeze
3463 self.eq.inotify_add_watch(testdir)
3464- self.eq.subscribe(DontHitMe())
3465+ self.eq.subscribe(DontHitMe(self))
3466 self.eq.freeze_begin(testdir)
3467
3468 open(testfile, "w").close()
3469@@ -1132,19 +1130,36 @@
3470 class MutedSignalsTests(BaseTwisted):
3471 """Test that EQ filter some signals on demand."""
3472
3473- class DontHitMe(object):
3474- """we shouldn't be called"""
3475- # class-closure, cannot use self, pylint: disable-msg=E0213
3476- def __init__(innerself, obj):
3477- innerself.obj = obj
3478- def handle_default(innerself, *a):
3479- """Something here? Error!"""
3480- innerself.obj.finished_error("don't hit me! received %s" % (a,))
3481-
3482 def check_filter(self, _=None):
3483 self.assertFalse(self.eq._processor._to_mute._cnt)
3484 self.finished_ok()
3485
3486+ def test_mute_and_remove(self):
3487+ """Test add and remove the mute."""
3488+ # add
3489+ self.eq.add_to_mute_filter('FS_FILE_OPEN', 'somepath')
3490+ self.assertEqual(self.eq._processor._to_mute._cnt,
3491+ {('FS_FILE_OPEN', 'somepath'): 1})
3492+ self.eq.add_to_mute_filter('FS_FILE_OPEN', 'somepath')
3493+ self.assertEqual(self.eq._processor._to_mute._cnt,
3494+ {('FS_FILE_OPEN', 'somepath'): 2})
3495+ self.eq.add_to_mute_filter('FS_FILE_OPEN', 'otherpath')
3496+ self.assertEqual(self.eq._processor._to_mute._cnt,
3497+ {('FS_FILE_OPEN', 'somepath'): 2,
3498+ ('FS_FILE_OPEN', 'otherpath'): 1})
3499+
3500+ # remove
3501+ self.eq.rm_from_mute_filter('FS_FILE_OPEN', 'somepath')
3502+ self.assertEqual(self.eq._processor._to_mute._cnt,
3503+ {('FS_FILE_OPEN', 'somepath'): 1,
3504+ ('FS_FILE_OPEN', 'otherpath'): 1})
3505+ self.eq.rm_from_mute_filter('FS_FILE_OPEN', 'otherpath')
3506+ self.assertEqual(self.eq._processor._to_mute._cnt,
3507+ {('FS_FILE_OPEN', 'somepath'): 1})
3508+ self.eq.rm_from_mute_filter('FS_FILE_OPEN', 'somepath')
3509+ self.assertEqual(self.eq._processor._to_mute._cnt, {})
3510+
3511+
3512 def test_file_open(self):
3513 """Test receiving the open signal on files."""
3514 testfile = os.path.join(self.root_dir, "foo")
3515@@ -1153,7 +1168,7 @@
3516 self.eq.add_to_mute_filter("FS_FILE_CLOSE_NOWRITE", testfile)
3517
3518 self.eq.inotify_add_watch(self.root_dir)
3519- self.eq.subscribe(self.DontHitMe(self))
3520+ self.eq.subscribe(DontHitMe(self))
3521
3522 # generate the event
3523 open(testfile)
3524@@ -1168,7 +1183,7 @@
3525 self.eq.add_to_mute_filter("FS_FILE_CLOSE_NOWRITE", testfile)
3526
3527 self.eq.inotify_add_watch(self.root_dir)
3528- self.eq.subscribe(self.DontHitMe(self))
3529+ self.eq.subscribe(DontHitMe(self))
3530
3531 # generate the event
3532 fh.close()
3533@@ -1183,7 +1198,7 @@
3534 self.eq.add_to_mute_filter("FS_FILE_CLOSE_WRITE", testfile)
3535
3536 self.eq.inotify_add_watch(self.root_dir)
3537- self.eq.subscribe(self.DontHitMe(self))
3538+ self.eq.subscribe(DontHitMe(self))
3539
3540 # generate the event
3541 open(testfile, "w").close()
3542@@ -1196,7 +1211,7 @@
3543 self.eq.add_to_mute_filter("FS_DIR_CREATE", testdir)
3544
3545 self.eq.inotify_add_watch(self.root_dir)
3546- self.eq.subscribe(self.DontHitMe(self))
3547+ self.eq.subscribe(DontHitMe(self))
3548
3549 # generate the event
3550 os.mkdir(testdir)
3551@@ -1210,7 +1225,7 @@
3552 self.eq.add_to_mute_filter("FS_FILE_DELETE", testfile)
3553
3554 self.eq.inotify_add_watch(self.root_dir)
3555- self.eq.subscribe(self.DontHitMe(self))
3556+ self.eq.subscribe(DontHitMe(self))
3557
3558 # generate the event
3559 os.remove(testfile)
3560@@ -1224,7 +1239,7 @@
3561 self.eq.add_to_mute_filter("FS_DIR_DELETE", testdir)
3562
3563 self.eq.inotify_add_watch(self.root_dir)
3564- self.eq.subscribe(self.DontHitMe(self))
3565+ self.eq.subscribe(DontHitMe(self))
3566
3567 # generate the event
3568 os.rmdir(testdir)
3569@@ -1243,7 +1258,7 @@
3570 self.eq.add_to_mute_filter("FS_FILE_MOVE", fromfile, tofile)
3571
3572 self.eq.inotify_add_watch(self.root_dir)
3573- self.eq.subscribe(self.DontHitMe(self))
3574+ self.eq.subscribe(DontHitMe(self))
3575
3576 # generate the event
3577 os.rename(fromfile, tofile)
3578@@ -1262,7 +1277,7 @@
3579 self.eq.add_to_mute_filter("FS_DIR_MOVE", fromdir, todir)
3580
3581 self.eq.inotify_add_watch(self.root_dir)
3582- self.eq.subscribe(self.DontHitMe(self))
3583+ self.eq.subscribe(DontHitMe(self))
3584
3585 # generate the event
3586 os.rename(fromdir, todir)
3587@@ -1281,7 +1296,7 @@
3588 self.eq.add_to_mute_filter("FS_FILE_MOVE", fromfile, tofile)
3589
3590 self.eq.inotify_add_watch(self.root_dir)
3591- self.eq.subscribe(self.DontHitMe(self))
3592+ self.eq.subscribe(DontHitMe(self))
3593
3594 # generate the event
3595 os.rename(fromfile, tofile)
3596@@ -1299,7 +1314,7 @@
3597 self.eq.add_to_mute_filter("FS_FILE_CLOSE_WRITE", tofile)
3598
3599 self.eq.inotify_add_watch(root_dir)
3600- self.eq.subscribe(self.DontHitMe(self))
3601+ self.eq.subscribe(DontHitMe(self))
3602
3603 # generate the event
3604 os.rename(fromfile, tofile)
3605@@ -1701,6 +1716,140 @@
3606 return self._deferred
3607
3608
3609+class NonUTF8NamesTests(BaseTwisted):
3610+ """Test the non-utf8 name handling."""
3611+
3612+ invalid_name = "invalid \xff\xff name"
3613+
3614+ def test_file_open(self):
3615+ """Test invalid_filename after open a file."""
3616+ testfile = os.path.join(self.root_dir, self.invalid_name)
3617+ open(testfile, "w").close()
3618+
3619+ self.eq.inotify_add_watch(self.root_dir)
3620+ should_events = [
3621+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # open
3622+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # close no w
3623+ ]
3624+ self.eq.subscribe(DynamicHitMe(should_events, self))
3625+
3626+ # generate the event
3627+ open(testfile)
3628+ return self._deferred
3629+
3630+ def test_file_close_nowrite(self):
3631+ """Test invalid_filename after a close no write."""
3632+ testfile = os.path.join(self.root_dir, self.invalid_name)
3633+ open(testfile, "w").close()
3634+ fh = open(testfile)
3635+
3636+ self.eq.inotify_add_watch(self.root_dir)
3637+ should_events = [
3638+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # close no w
3639+ ]
3640+ self.eq.subscribe(DynamicHitMe(should_events, self))
3641+
3642+ # generate the event
3643+ fh.close()
3644+ return self._deferred
3645+
3646+ def test_file_create_close_write(self):
3647+ """Test invalid_filename after a create, open and close write."""
3648+ testfile = os.path.join(self.root_dir, self.invalid_name)
3649+
3650+ self.eq.inotify_add_watch(self.root_dir)
3651+ should_events = [
3652+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # create
3653+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # open
3654+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # close w
3655+ ]
3656+ self.eq.subscribe(DynamicHitMe(should_events, self))
3657+
3658+ # generate the event
3659+ open(testfile, "w").close()
3660+ return self._deferred
3661+
3662+ def test_dir_create(self):
3663+ """Test invalid_filename after a dir create."""
3664+ testdir = os.path.join(self.root_dir, self.invalid_name)
3665+
3666+ self.eq.inotify_add_watch(self.root_dir)
3667+ should_events = [
3668+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # create
3669+ ]
3670+ self.eq.subscribe(DynamicHitMe(should_events, self))
3671+
3672+ # generate the event
3673+ os.mkdir(testdir)
3674+ return self._deferred
3675+
3676+ def test_file_delete(self):
3677+ """Test invalid_filename after a file delete."""
3678+ testfile = os.path.join(self.root_dir, self.invalid_name)
3679+ open(testfile, "w").close()
3680+
3681+ self.eq.inotify_add_watch(self.root_dir)
3682+ should_events = [
3683+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # delete
3684+ ]
3685+ self.eq.subscribe(DynamicHitMe(should_events, self))
3686+
3687+ # generate the event
3688+ os.remove(testfile)
3689+ return self._deferred
3690+
3691+ def test_dir_delete(self):
3692+ """Test invalid_filename after a dir delete."""
3693+ testdir = os.path.join(self.root_dir, self.invalid_name)
3694+ os.mkdir(testdir)
3695+
3696+ self.eq.inotify_add_watch(self.root_dir)
3697+ should_events = [
3698+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # delete
3699+ ]
3700+ self.eq.subscribe(DynamicHitMe(should_events, self))
3701+
3702+ # generate the event
3703+ os.rmdir(testdir)
3704+ return self._deferred
3705+
3706+ def test_file_move_to(self):
3707+ """Test invalid_filename after moving a file into a watched dir."""
3708+ fromfile = os.path.join(self.root_dir, self.invalid_name)
3709+ open(fromfile, "w").close()
3710+ destdir = os.path.join(self.root_dir, "watched_dir")
3711+ os.mkdir(destdir)
3712+ destfile = os.path.join(destdir, self.invalid_name)
3713+
3714+ self.eq.inotify_add_watch(destdir)
3715+ should_events = [
3716+ ('FS_INVALID_NAME', destdir, self.invalid_name), # move to
3717+ ]
3718+ self.eq.subscribe(DynamicHitMe(should_events, self))
3719+
3720+ # generate the event
3721+ os.rename(fromfile, destfile)
3722+ return self._deferred
3723+
3724+ def test_file_move_from(self):
3725+ """Test invalid_filename after moving a file from a watched dir."""
3726+ fromdir = os.path.join(self.root_dir, "watched_dir")
3727+ os.mkdir(fromdir)
3728+ fromfile = os.path.join(fromdir, self.invalid_name)
3729+ open(fromfile, "w").close()
3730+ destfile = os.path.join(self.root_dir, self.invalid_name)
3731+
3732+ self.eq.inotify_add_watch(fromdir)
3733+ should_events = [
3734+ ('FS_INVALID_NAME', fromdir, self.invalid_name), # move from
3735+ ]
3736+ self.eq.subscribe(DynamicHitMe(should_events, self))
3737+
3738+ # generate the event
3739+ os.rename(fromfile, destfile)
3740+ return self._deferred
3741+
3742+
3743 def test_suite():
3744 # pylint: disable-msg=C0111
3745 return unittest.TestLoader().loadTestsFromName(__name__)
3746
3747=== modified file 'tests/syncdaemon/test_eq_inotify.pyc'
3748Binary files tests/syncdaemon/test_eq_inotify.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_eq_inotify.pyc 2010-03-31 23:55:45 +0000 differ
3749=== modified file 'tests/syncdaemon/test_eventqueue.py'
3750--- tests/syncdaemon/test_eventqueue.py 2010-03-04 16:47:43 +0000
3751+++ tests/syncdaemon/test_eventqueue.py 2010-03-31 23:55:45 +0000
3752@@ -382,6 +382,42 @@
3753 self.assertFalse(self.mf._cnt)
3754
3755
3756+class IgnoreFileTests(unittest.TestCase):
3757+ """Tests the ignore files behaviour."""
3758+
3759+ def test_filter_none(self):
3760+ """Still works ok even if not receiving a regex to ignore."""
3761+ p = event_queue._GeneralINotifyProcessor(None)
3762+ self.assertFalse(p.is_ignored("froo.pyc"))
3763+
3764+ def test_filter_one(self):
3765+ """Filters stuff that matches (or not) this one regex."""
3766+ p = event_queue._GeneralINotifyProcessor(None, ['\A.*\\.pyc\Z'])
3767+ self.assertTrue(p.is_ignored("froo.pyc"))
3768+ self.assertFalse(p.is_ignored("froo.pyc.real"))
3769+ self.assertFalse(p.is_ignored("otherstuff"))
3770+
3771+ def test_filter_two_simple(self):
3772+ """Filters stuff that matches (or not) these simple regexes."""
3773+ p = event_queue._GeneralINotifyProcessor(None, ['\A.*foo\Z',
3774+ '\A.*bar\Z'])
3775+ self.assertTrue(p.is_ignored("blah_foo"))
3776+ self.assertTrue(p.is_ignored("blah_bar"))
3777+ self.assertFalse(p.is_ignored("bar_xxx"))
3778+ self.assertFalse(p.is_ignored("--foo--"))
3779+ self.assertFalse(p.is_ignored("otherstuff"))
3780+
3781+ def test_filter_two_complex(self):
3782+ """Filters stuff that matches (or not) these complex regexes."""
3783+ p = event_queue._GeneralINotifyProcessor(None, ['\A.*foo\Z|\Afoo.*\Z',
3784+ '\A.*bar\Z'])
3785+ self.assertTrue(p.is_ignored("blah_foo"))
3786+ self.assertTrue(p.is_ignored("blah_bar"))
3787+ self.assertTrue(p.is_ignored("foo_xxx"))
3788+ self.assertFalse(p.is_ignored("--foo--"))
3789+ self.assertFalse(p.is_ignored("otherstuff"))
3790+
3791+
3792 def test_suite():
3793 # pylint: disable-msg=C0111
3794 return unittest.TestLoader().loadTestsFromName(__name__)
3795
3796=== modified file 'tests/syncdaemon/test_eventqueue.pyc'
3797Binary files tests/syncdaemon/test_eventqueue.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_eventqueue.pyc 2010-03-31 23:55:45 +0000 differ
3798=== modified file 'tests/syncdaemon/test_eventsnanny.pyc'
3799Binary files tests/syncdaemon/test_eventsnanny.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_eventsnanny.pyc 2010-03-31 23:55:45 +0000 differ
3800=== modified file 'tests/syncdaemon/test_fileshelf.py'
3801--- tests/syncdaemon/test_fileshelf.py 2010-02-17 23:51:29 +0000
3802+++ tests/syncdaemon/test_fileshelf.py 2010-03-31 23:55:45 +0000
3803@@ -133,7 +133,7 @@
3804 self.assertTrue(('foo', 'bar') and ('foo1', 'bar1') in \
3805 [(k, v) for k, v in shelf.items()])
3806
3807- def test_broked_metadata_without_backup(self):
3808+ def test_broken_metadata_without_backup(self):
3809 """test the shelf behavior when it hit a broken metadata file without
3810 backup.
3811 """
3812@@ -148,7 +148,7 @@
3813 f.write(BROKEN_PICKLE)
3814 self.assertRaises(KeyError, self.shelf.__getitem__, 'broken_pickle')
3815
3816- def test_broked_metadata_with_backup(self):
3817+ def test_broken_metadata_with_backup(self):
3818 """test that each time a metadata file is updated a .old is kept"""
3819 self.shelf['bad_file'] = {'value':'old'}
3820 path = self.shelf.key_file('bad_file')
3821@@ -254,6 +254,42 @@
3822 self.assertEquals(shelf.values['foo'],
3823 cPickle.dumps('bar', protocol=2))
3824
3825+ def test_broken_metadata_iteritems(self):
3826+ """Test that broken metadata is ignored during iteritems."""
3827+ self.shelf['ok_key'] = {'status':'this is valid metadata'}
3828+ self.shelf['bad_file'] = {}
3829+ path = self.shelf.key_file('bad_file')
3830+ open(path, 'w').close()
3831+ self.assertRaises(KeyError, self.shelf.__getitem__, 'bad_file')
3832+ self.assertEquals(1, len(list(self.shelf.iteritems())))
3833+ self.assertFalse(os.path.exists(path))
3834+
3835+ self.shelf['broken_pickle'] = {}
3836+ path = self.shelf.key_file('broken_pickle')
3837+ with open(path, 'w') as f:
3838+ f.write(BROKEN_PICKLE)
3839+ self.assertRaises(KeyError, self.shelf.__getitem__, 'broken_pickle')
3840+ self.assertEquals(1, len(list(self.shelf.iteritems())))
3841+ self.assertFalse(os.path.exists(path))
3842+
3843+ def test_broken_metadata_items(self):
3844+ """Test that broken metadata is ignored during iteritems."""
3845+ self.shelf['ok_key'] = {'status':'this is valid metadata'}
3846+ self.shelf['bad_file'] = {}
3847+ path = self.shelf.key_file('bad_file')
3848+ open(path, 'w').close()
3849+ self.assertRaises(KeyError, self.shelf.__getitem__, 'bad_file')
3850+ self.assertEquals(1, len(list(self.shelf.items())))
3851+ self.assertFalse(os.path.exists(path))
3852+
3853+ self.shelf['broken_pickle'] = {}
3854+ path = self.shelf.key_file('broken_pickle')
3855+ with open(path, 'w') as f:
3856+ f.write(BROKEN_PICKLE)
3857+ self.assertRaises(KeyError, self.shelf.__getitem__, 'broken_pickle')
3858+ self.assertEquals(1, len(list(self.shelf.items())))
3859+ self.assertFalse(os.path.exists(path))
3860+
3861
3862 class CachedFileShelfTests(TestFileShelf):
3863 """TestFileShelf tests but using CachedFileShelf"""
3864@@ -273,7 +309,7 @@
3865 self.shelf['realkey']
3866 self.assertEquals(self.shelf.cache_hits, 1)
3867
3868- def test_broked_metadata_with_backup(self):
3869+ def test_broken_metadata_with_backup(self):
3870 """overrides parent test as we have the value in the cache."""
3871 self.shelf['bad_file'] = {'value':'old'}
3872 path = self.shelf.key_file('bad_file')
3873
3874=== modified file 'tests/syncdaemon/test_fileshelf.pyc'
3875Binary files tests/syncdaemon/test_fileshelf.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_fileshelf.pyc 2010-03-31 23:55:45 +0000 differ
3876=== modified file 'tests/syncdaemon/test_fsm.py'
3877--- tests/syncdaemon/test_fsm.py 2010-02-17 23:51:29 +0000
3878+++ tests/syncdaemon/test_fsm.py 2010-03-31 23:55:45 +0000
3879@@ -24,12 +24,11 @@
3880 import shutil
3881 import unittest
3882 import time
3883-import logging
3884
3885 from contrib.testing.testcase import (
3886 FakeVolumeManager,
3887 FakeMain,
3888- MementoHandler,
3889+ MementoHandler
3890 )
3891
3892 from ubuntuone.syncdaemon.filesystem_manager import (
3893@@ -39,7 +38,6 @@
3894 METADATA_VERSION,
3895 )
3896 from ubuntuone.syncdaemon.event_queue import EventQueue
3897-from ubuntuone.syncdaemon.logger import LOGFILENAME
3898 from ubuntuone.syncdaemon import logger
3899 from ubuntuone.syncdaemon.volume_manager import Share, allow_writes
3900
3901@@ -74,11 +72,19 @@
3902 self.fsm, self.shares_dir)
3903 self.share_path = self.share.path
3904
3905+ # add a in-memory logger handler
3906+ self.memento = MementoHandler()
3907+ self.memento.setLevel(0)
3908+ logger.root_logger.addHandler(self.memento)
3909+
3910 def tearDown(self):
3911 """ Clean up the tests. """
3912 self.eq.shutdown()
3913 self.rmtree(TESTS_DIR)
3914
3915+ # remove the handler
3916+ logger.root_logger.removeHandler(self.memento)
3917+
3918 @staticmethod
3919 def rmtree(path):
3920 """rmtree that handle ro childs."""
3921@@ -101,6 +107,16 @@
3922 fsm.vm.add_share(share)
3923 return share
3924
3925+ def create_node(self, name, is_dir=False, share=None):
3926+ """Create a node."""
3927+ if share is None:
3928+ share = self.share
3929+ path = os.path.join(share.path, name)
3930+ mdid = self.fsm.create(path, share.volume_id, is_dir=is_dir)
3931+ self.fsm.set_node_id(path, "uuid1")
3932+ mdobj = self.fsm.get_by_mdid(mdid)
3933+ return mdobj
3934+
3935
3936 class StartupTests(unittest.TestCase):
3937 """Test the basic startup behaviour."""
3938@@ -1238,17 +1254,89 @@
3939 self.fsm._set_node_id, mdobj, 'bad-uuid', path)
3940
3941
3942+class GetMDObjectsInDirTests(FSMTestCase):
3943+ """Test the get_mdobjs_in_dir method."""
3944+
3945+ def create_some_contents(self, share):
3946+ a = 'a'
3947+ ab = os.path.join(a, 'b')
3948+ ab1 = os.path.join(a, 'b1')
3949+ ab2 = os.path.join(a, 'b2')
3950+ ac = os.path.join(a, 'c')
3951+ acd = os.path.join(ac, 'd')
3952+
3953+ dirs = [a, ab, ab1, ab2, ac, acd]
3954+ for d in dirs:
3955+ self.create_node(d, is_dir=True, share=share)
3956+
3957+ x = os.path.join(a, 'x.txt')
3958+ y = os.path.join(ab, 'y.txt')
3959+ z = os.path.join(ac, 'z.txt')
3960+
3961+ files = [x, y, z]
3962+ for f in files:
3963+ self.create_node(f, is_dir=False, share=share)
3964+
3965+ self.contents[share] = sorted(dirs + files)
3966+
3967+ def setUp(self):
3968+ """Init."""
3969+ FSMTestCase.setUp(self)
3970+ self.contents = {}
3971+ self.create_some_contents(self.share)
3972+
3973+ def tearDown(self):
3974+ """Clean up."""
3975+ self.contents = {}
3976+ FSMTestCase.tearDown(self)
3977+
3978+ def test_basic(self):
3979+ """Test basic retrieval."""
3980+ expected = ['a']
3981+ actual = sorted([d.path for d in
3982+ self.fsm.get_mdobjs_in_dir(self.share.path)])
3983+ self.assertEqual(expected, actual)
3984+
3985+ def test_no_tree(self):
3986+ """Test just receiving the dir and not the tree."""
3987+ expected = ['a/b', 'a/b1', 'a/b2', 'a/c', 'a/x.txt']
3988+ actual = sorted([d.path for d in self.fsm.get_mdobjs_in_dir(
3989+ os.path.join(self.share.path, 'a'))])
3990+ self.assertEqual(expected, actual)
3991+
3992+ def test_similar_paths(self):
3993+ """Test having similar paths (a/b, a/b1, a/b2)."""
3994+ expected = ['a/b/y.txt']
3995+ actual = sorted([d.path for d in self.fsm.get_mdobjs_in_dir(
3996+ os.path.join(self.share.path, 'a', 'b'))])
3997+ self.assertEqual(expected, actual)
3998+
3999+ def test_with_two_shares(self):
4000+ """Test having 2 shares."""
4001+ second_share = self.create_share('second_share', 'the_second',
4002+ self.fsm, self.shares_dir)
4003+ self.create_some_contents(second_share)
4004+
4005+ expected = ['a']
4006+ actual = sorted([d.path for d in
4007+ self.fsm.get_mdobjs_in_dir(second_share.path)])
4008+ self.assertEqual(expected, actual)
4009+
4010+ def test_both_shares(self):
4011+ """Test having 2 shares and asking for mdobjs in shares_dir."""
4012+ second_share = self.create_share('second_share', 'the_second',
4013+ self.fsm, self.shares_dir)
4014+ self.create_some_contents(second_share)
4015+
4016+ expected = []
4017+ actual = sorted([d.path for d in
4018+ self.fsm.get_mdobjs_in_dir(self.shares_dir)])
4019+ self.assertEqual(expected, actual)
4020+
4021+
4022 class StatTests(FSMTestCase):
4023 """Test all the behaviour regarding the stats."""
4024
4025- def create_node(self, name):
4026- """Creates a node."""
4027- path = os.path.join(self.share.path, name)
4028- mdid = self.fsm.create(path, self.share.volume_id)
4029- self.fsm.set_node_id(path, "uuid1")
4030- mdobj = self.fsm.get_by_mdid(mdid)
4031- return mdobj
4032-
4033 def test_create_nofile(self):
4034 """Test creation when there's no file."""
4035 mdobj = self.create_node("foo")
4036@@ -1866,13 +1954,48 @@
4037 assert self.fsm.changed(path=local_file) == self.fsm.CHANGED_LOCAL
4038 self.assertRaises(OSError, self.fsm.delete_file, local_dir)
4039
4040+ def test_delete_dir_when_non_empty_and_prior_conflict_on_file(self):
4041+ """Test that a dir is not deleted, when there is a conflicted file."""
4042+ # local directory
4043+ local_dir = os.path.join(self.root_dir, "foo")
4044+ os.mkdir(local_dir)
4045+ self.fsm.create(local_dir, "", is_dir=True)
4046+ self.fsm.set_node_id(local_dir, "uuid")
4047+
4048+ local_file = os.path.join(local_dir,
4049+ "bar.txt" + self.fsm.CONFLICT_SUFFIX)
4050+ open(local_file, 'w').close() # touch bar.txt.u1conflict
4051+
4052+ assert not local_file in self.fsm._idx_path
4053+ self.assertRaises(OSError, self.fsm.delete_file, local_dir)
4054+
4055+ infos = [record.message for record in self.memento.records
4056+ if record.levelname == 'INFO']
4057+ self.assertTrue(len(infos) == 1)
4058+ self.assertTrue(local_file in infos[0])
4059+
4060+ def test_delete_dir_when_non_empty_and_prior_conflict_on_subdir(self):
4061+ """Test that a dir is not deleted, when there is a conflicted dir."""
4062+ # local directory
4063+ local_dir = os.path.join(self.root_dir, "foo")
4064+ os.mkdir(local_dir)
4065+ self.fsm.create(local_dir, "", is_dir=True)
4066+ self.fsm.set_node_id(local_dir, "uuid")
4067+
4068+ local_subdir = os.path.join(local_dir,
4069+ "subdir_bar" + self.fsm.CONFLICT_SUFFIX)
4070+ os.mkdir(local_subdir)
4071+
4072+ assert not local_subdir in self.fsm._idx_path
4073+ self.assertRaises(OSError, self.fsm.delete_file, local_dir)
4074+
4075+ infos = [record.message for record in self.memento.records
4076+ if record.levelname == 'INFO']
4077+ self.assertTrue(len(infos) == 1)
4078+ self.assertTrue(local_subdir in infos[0])
4079+
4080 def test_no_warning_on_log_file_when_recursive_delete(self):
4081 """Test that sucessfully deleted dir does not log OSError."""
4082- # add a in-memory handler
4083- memento = MementoHandler()
4084- memento.setLevel(logging.WARNING)
4085- logger.root_logger.addHandler(memento)
4086-
4087 local_dir = os.path.join(self.root_dir, "foo")
4088 os.mkdir(local_dir)
4089 self.fsm.create(local_dir, "", is_dir=True)
4090@@ -1883,20 +2006,15 @@
4091 self.fsm.create(local_file, "")
4092 self.fsm.set_node_id(local_file, "uuid_file")
4093
4094+ previous = self.memento.records
4095 self.fsm.delete_file(local_dir)
4096
4097- # remove the handler
4098- logger.root_logger.removeHandler(memento)
4099- self.assertFalse(memento.records, memento.records)
4100+ # no logs were added
4101+ self.assertEquals(previous, self.memento.records)
4102
4103- # FIXME: Bug #494469
4104- def SKIP_test_warning_on_log_file_when_failing_delete(self):
4105+ def test_warning_on_log_file_when_failing_delete(self):
4106 """Test that sucessfully deleted dir does not log OSError."""
4107
4108- log = open(LOGFILENAME, 'r')
4109- log.flush()
4110- log.read() # ignore log content till now
4111-
4112 local_dir = os.path.join(self.root_dir, "foo")
4113 self.fsm.create(local_dir, "", is_dir=True)
4114 self.fsm.set_node_id(local_dir, "uuid")
4115@@ -1904,11 +2022,11 @@
4116 # local_dir does not exist on the file system
4117 self.fsm.delete_file(local_dir)
4118
4119- log.flush()
4120- log_content = log.read()
4121- log.close()
4122- log_present = 'OSError [Errno 2] No such file or directory' in log_content
4123- self.assertTrue(log_present)
4124+ warnings = [record.message for record in self.memento.records
4125+ if record.levelname == 'WARNING']
4126+ self.assertTrue(len(warnings) == 1)
4127+ self.assertTrue('OSError [Errno 2]' in warnings[0])
4128+ self.assertTrue(local_dir in warnings[0])
4129
4130 def test_move_dir_to_conflict(self):
4131 """Test that the conflict to a dir removes children metadata."""
4132@@ -2779,6 +2897,162 @@
4133 self.assertFalse(("", "uuid3", "") in data)
4134
4135
4136+class MutingTestCase(FSMTestCase):
4137+ """Test FSM interaction with mutes."""
4138+
4139+ def setUp(self):
4140+ """Set up the test infrastructure."""
4141+ FSMTestCase.setUp(self)
4142+ self.muted = []
4143+
4144+ # in-the-middle add
4145+ self._orig_add_mute = self.eq.add_to_mute_filter
4146+ def _fake_add(*a):
4147+ """Store what is added."""
4148+ self.muted.append(a)
4149+ return self._orig_add_mute(*a)
4150+ self.eq.add_to_mute_filter = _fake_add
4151+
4152+ # in-the-middle remove
4153+ self._orig_rm_mute = self.eq.rm_from_mute_filter
4154+ def _fake_rm(*a):
4155+ """Store what is deleted."""
4156+ self.muted.remove(a)
4157+ return self._orig_rm_mute(*a)
4158+ self.eq.rm_from_mute_filter = _fake_rm
4159+
4160+ def tearDown(self):
4161+ """Put original stuff back in."""
4162+ FSMTestCase.tearDown(self)
4163+ self.eq.add_to_mute_filter = self._orig_add_mute
4164+ self.eq.rm_from_mute_filter = self._orig_rm_mute
4165+
4166+ def test_movefile_ok(self):
4167+ """Move file adds a mute filter."""
4168+ path1 = os.path.join(self.share.path, "thisfile1")
4169+ path2 = os.path.join(self.share.path, "thisfile2")
4170+ open(path1, "w").close()
4171+ self.create_node(path1)
4172+
4173+ # move and check
4174+ self.fsm.move_file("share", path1, path2)
4175+ self.assertEqual(self.muted, [('FS_FILE_MOVE', path1, path2)])
4176+
4177+ def test_movefile_error(self):
4178+ """Move file adds and removes a mute filter."""
4179+ path1 = os.path.join(self.share.path, "thisfile1")
4180+ path2 = os.path.join(self.share.path, "thisfile2")
4181+ self.create_node(path1)
4182+
4183+ # move and check
4184+ self.fsm.move_file("share", path1, path2)
4185+ self.assertEqual(self.muted, [])
4186+
4187+ def test_movedir_ok(self):
4188+ """Move dir adds a mute filter."""
4189+ path1 = os.path.join(self.share.path, "thisfile1")
4190+ path2 = os.path.join(self.share.path, "thisfile2")
4191+ os.mkdir(path1)
4192+ self.create_node(path1, is_dir=True)
4193+
4194+ # move and check
4195+ self.fsm.move_file("share", path1, path2)
4196+ self.assertEqual(self.muted, [('FS_DIR_MOVE', path1, path2)])
4197+
4198+ def test_movedir_error(self):
4199+ """Move dir adds and removes a mute filter."""
4200+ path1 = os.path.join(self.share.path, "thisfile1")
4201+ path2 = os.path.join(self.share.path, "thisfile2")
4202+ self.create_node(path1, is_dir=True)
4203+
4204+ # move and check
4205+ self.fsm.move_file("share", path1, path2)
4206+ self.assertEqual(self.muted, [])
4207+
4208+ def test_deletefile_ok(self):
4209+ """Delete file adds a mute filter."""
4210+ testfile = os.path.join(self.share_path, "path")
4211+ open(testfile, "w").close()
4212+ self.fsm.create(testfile, "share")
4213+ self.fsm.set_node_id(testfile, "uuid")
4214+
4215+ # delete and check
4216+ self.fsm.delete_file(testfile)
4217+ self.assertEqual(self.muted, [('FS_FILE_DELETE', testfile)])
4218+
4219+ def test_deletefile_error(self):
4220+ """Delete file adds and removes a mute filter."""
4221+ testfile = os.path.join(self.share_path, "path")
4222+ self.fsm.create(testfile, "share")
4223+ self.fsm.set_node_id(testfile, "uuid")
4224+
4225+ # delete and check
4226+ self.fsm.delete_file(testfile)
4227+ self.assertEqual(self.muted, [])
4228+
4229+ def test_deletedir_ok(self):
4230+ """Delete dir adds a mute filter."""
4231+ testfile = os.path.join(self.share_path, "path")
4232+ os.mkdir(testfile)
4233+ self.fsm.create(testfile, "share", is_dir=True)
4234+ self.fsm.set_node_id(testfile, "uuid")
4235+
4236+ # delete and check
4237+ self.fsm.delete_file(testfile)
4238+ self.assertEqual(self.muted, [('FS_DIR_DELETE', testfile)])
4239+
4240+ def test_deletedir_error(self):
4241+ """Delete dir adds and removes a mute filter."""
4242+ testfile = os.path.join(self.share_path, "path")
4243+ self.fsm.create(testfile, "share", is_dir=True)
4244+ self.fsm.set_node_id(testfile, "uuid")
4245+
4246+ # delete and check
4247+ self.fsm.delete_file(testfile)
4248+ self.assertEqual(self.muted, [])
4249+
4250+ def test_conflict_movefile_ok(self):
4251+ """Move file when conflict adds a mute filter."""
4252+ path1 = os.path.join(self.share.path, "thisfile1")
4253+ open(path1, "w").close()
4254+ mdobj = self.create_node(path1)
4255+
4256+ # move and check
4257+ self.fsm.move_to_conflict(mdobj.mdid)
4258+ self.assertEqual(self.muted,
4259+ [('FS_FILE_MOVE', path1, path1 + '.u1conflict')])
4260+
4261+ def test_conflict_movefile_error(self):
4262+ """Move file when conflict adds and removes a mute filter."""
4263+ path1 = os.path.join(self.share.path, "thisfile1")
4264+ mdobj = self.create_node(path1)
4265+
4266+ # move and check
4267+ self.fsm.move_to_conflict(mdobj.mdid)
4268+ self.assertEqual(self.muted, [])
4269+
4270+ def test_conflict_movedir_ok(self):
4271+ """Move dir when conflict adds a mute filter."""
4272+ path1 = os.path.join(self.share.path, "thisfile1")
4273+ os.mkdir(path1)
4274+ mdobj = self.create_node(path1, is_dir=True)
4275+
4276+ # move and check
4277+ self.fsm.move_to_conflict(mdobj.mdid)
4278+ self.assertEqual(self.muted,
4279+ [('FS_DIR_MOVE', path1, path1 + '.u1conflict')])
4280+
4281+ def test_conflict_movedir_error(self):
4282+ """Move dir when conflict adds and removes a mute filter."""
4283+ path1 = os.path.join(self.share.path, "thisfile1")
4284+ mdobj = self.create_node(path1, is_dir=True)
4285+
4286+ # move and check
4287+ self.fsm.move_to_conflict(mdobj.mdid)
4288+ self.assertEqual(self.muted, [])
4289+
4290+
4291+
4292 def test_suite():
4293 # pylint: disable-msg=C0111
4294 return unittest.TestLoader().loadTestsFromName(__name__)
4295
4296=== modified file 'tests/syncdaemon/test_fsm.pyc'
4297Binary files tests/syncdaemon/test_fsm.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_fsm.pyc 2010-03-31 23:55:45 +0000 differ
4298=== modified file 'tests/syncdaemon/test_hashqueue.py'
4299--- tests/syncdaemon/test_hashqueue.py 2010-02-17 23:51:29 +0000
4300+++ tests/syncdaemon/test_hashqueue.py 2010-03-31 23:55:45 +0000
4301@@ -531,7 +531,7 @@
4302 receiver = Helper()
4303 hq = hash_queue.HashQueue(receiver)
4304 hq.shutdown()
4305- self.assertRaises(RuntimeError, hq.insert, 'foo', "mdid")
4306+ self.assertTrue(hq._stopped)
4307
4308 def test_shutdown_while_hashing(self):
4309 """Test that the HashQueue is shutdown ASAP while it's hashing."""
4310@@ -573,7 +573,8 @@
4311 receiver = Helper()
4312 hq = hash_queue.HashQueue(receiver)
4313 hq.shutdown()
4314- self.assertRaises(RuntimeError, hq.insert, '/foo/bar', "mdid")
4315+ hq.insert('foo', 'mdid')
4316+ self.assertFalse(hq.is_hashing('foo', 'mdid'))
4317
4318
4319 class UniqueQueueTests(TwistedTestCase):
4320
4321=== modified file 'tests/syncdaemon/test_hashqueue.pyc'
4322Binary files tests/syncdaemon/test_hashqueue.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_hashqueue.pyc 2010-03-31 23:55:45 +0000 differ
4323=== modified file 'tests/syncdaemon/test_localrescan.py'
4324--- tests/syncdaemon/test_localrescan.py 2010-03-04 16:47:43 +0000
4325+++ tests/syncdaemon/test_localrescan.py 2010-03-31 23:55:45 +0000
4326@@ -48,7 +48,7 @@
4327 def _fake(self, *a):
4328 """fake"""
4329 inotify_add_watch = inotify_has_watch = freeze_rollback = is_frozen = _fake
4330- freeze_begin = add_to_mute_filter = _fake
4331+ inotify_rm_watch = freeze_begin = add_to_mute_filter = _fake
4332
4333 def freeze_commit(self, events):
4334 """just store events"""
4335@@ -302,6 +302,42 @@
4336 self.assertTrue(isinstance(d, defer.Deferred))
4337 return d
4338
4339+ def test_start_without_udf_itself(self):
4340+ """LR start() having removed UDFs."""
4341+ vol_to_unsub = self.volumes[0]
4342+ vol_to_keep = self.volumes[1:]
4343+ shutil.rmtree(vol_to_unsub.path)
4344+ d = self.lr.start()
4345+
4346+ def check(_):
4347+ """Removed UDF should be desubscribed."""
4348+ # these should remain ok
4349+ for vol in vol_to_keep:
4350+ self.assertTrue(vol.subscribed)
4351+ # this should be unsubscribed
4352+ self.assertFalse(vol_to_unsub.subscribed)
4353+
4354+ d.addCallback(check)
4355+ return d
4356+
4357+ def test_start_without_udf_ancestors(self):
4358+ """LR start() having removed UDFs parents."""
4359+ vol_to_unsub = self.volumes[-1] # grab the one that has lot of parents
4360+ vol_to_keep = self.volumes[:-1]
4361+ shutil.rmtree(os.path.dirname(vol_to_unsub.path))
4362+ d = self.lr.start()
4363+
4364+ def check(_):
4365+ """Removed UDF should be desubscribed."""
4366+ # these should remain ok
4367+ for vol in vol_to_keep:
4368+ self.assertTrue(vol.subscribed)
4369+ # this should be unsubscribed
4370+ self.assertFalse(vol_to_unsub.subscribed)
4371+
4372+ d.addCallback(check)
4373+ return d
4374+
4375
4376 class TwistedBase(BaseTestCase):
4377 """Base class for twisted tests."""
4378@@ -503,6 +539,20 @@
4379 self.scanTest(check)
4380 return self.deferred
4381
4382+ def test_no_file_no_hash(self):
4383+ """Test useless metadata."""
4384+ path = os.path.join(self.share.path, "b")
4385+ self.fsm.create(path, self.share.volume_id)
4386+ self.assertTrue(self.fsm.has_metadata(path=path))
4387+
4388+ def check(_):
4389+ """Check."""
4390+ self.assertEqual(self.eq.pushed, [])
4391+ self.assertFalse(self.fsm.has_metadata(path=path))
4392+
4393+ self.startTest(check)
4394+ return self.deferred
4395+
4396 def test_disc_less_dir_normal(self):
4397 """Test having less in disc than in metadata, normal volume."""
4398 # create a node in the share, but no in disk
4399
4400=== modified file 'tests/syncdaemon/test_localrescan.pyc'
4401Binary files tests/syncdaemon/test_localrescan.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_localrescan.pyc 2010-03-31 23:55:45 +0000 differ
4402=== modified file 'tests/syncdaemon/test_logger.pyc'
4403Binary files tests/syncdaemon/test_logger.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_logger.pyc 2010-03-31 23:55:45 +0000 differ
4404=== modified file 'tests/syncdaemon/test_main.pyc'
4405Binary files tests/syncdaemon/test_main.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_main.pyc 2010-03-31 23:55:45 +0000 differ
4406=== modified file 'tests/syncdaemon/test_states.py'
4407--- tests/syncdaemon/test_states.py 2010-03-04 16:47:43 +0000
4408+++ tests/syncdaemon/test_states.py 2010-03-31 23:55:45 +0000
4409@@ -21,6 +21,7 @@
4410 from twisted.internet import defer, reactor
4411 from twisted.trial.unittest import TestCase as TwistedTestCase
4412
4413+from contrib.testing.testcase import FakeLogger
4414 from ubuntuone.syncdaemon.states import (
4415 StateManager, ConnectionManager, QueueManager, Node
4416 )
4417@@ -121,27 +122,6 @@
4418 return object.__getattribute__(self, name)
4419
4420
4421-
4422-class FakeLogger(object):
4423- """Helper logging class."""
4424- def __init__(self):
4425- self.logged = dict(debug=[], warning=[])
4426-
4427- def _log(self, log, txt, args):
4428- """Really logs."""
4429- if args:
4430- txt = txt % args
4431- log.append(txt)
4432-
4433- def warning(self, txt, *args):
4434- """WARNING logs."""
4435- self._log(self.logged['warning'], txt, args)
4436-
4437- def debug(self, txt, *args):
4438- """DEBUG logs."""
4439- self._log(self.logged['debug'], txt, args)
4440-
4441-
4442 class Base(TwistedTestCase):
4443 """Base class for state tests."""
4444 def setUp(self):
4445@@ -959,6 +939,13 @@
4446 d.append(self.check(node, 'SYS_UNKNOWN_ERROR', 'UNKNOWN_ERROR'))
4447 return defer.DeferredList(d)
4448
4449+ def test_root_mismatch_error(self):
4450+ """All nodes go to root_mismatch."""
4451+ d = []
4452+ for node in self.sm_nodes_ok:
4453+ d.append(self.check(node, 'SYS_ROOT_MISMATCH', 'ROOT_MISMATCH'))
4454+ return defer.DeferredList(d)
4455+
4456 def test_not_exiting_from_errors(self):
4457 """No return from errors."""
4458 d = []
4459
4460=== modified file 'tests/syncdaemon/test_states.pyc'
4461Binary files tests/syncdaemon/test_states.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_states.pyc 2010-03-31 23:55:45 +0000 differ
4462=== modified file 'tests/syncdaemon/test_sync.pyc'
4463Binary files tests/syncdaemon/test_sync.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_sync.pyc 2010-03-31 23:55:45 +0000 differ
4464=== modified file 'tests/syncdaemon/test_tools.pyc'
4465Binary files tests/syncdaemon/test_tools.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_tools.pyc 2010-03-31 23:55:45 +0000 differ
4466=== modified file 'tests/syncdaemon/test_u1fsfsm.pyc'
4467Binary files tests/syncdaemon/test_u1fsfsm.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_u1fsfsm.pyc 2010-03-31 23:55:45 +0000 differ
4468=== modified file 'tests/syncdaemon/test_u1sdtool.py'
4469--- tests/syncdaemon/test_u1sdtool.py 2010-03-10 23:36:53 +0000
4470+++ tests/syncdaemon/test_u1sdtool.py 2010-03-31 23:55:45 +0000
4471@@ -103,6 +103,14 @@
4472
4473 def test_show_path_info_unicode(self):
4474 """test the output of --info with unicode paths """
4475+ return self.generic_test_show_path_info_unicode('utf-8')
4476+
4477+ def test_show_path_info_unicode_pipe(self):
4478+ """test the output of --info with unicode paths going to e.g. a pipe"""
4479+ return self.generic_test_show_path_info_unicode(None)
4480+
4481+ def generic_test_show_path_info_unicode(self, encoding):
4482+ """generic test for the output of --info with unicode paths """
4483 path = os.path.join(self.root_dir, "ñoño")
4484 mdid = self.fs_manager.create(path, "")
4485 self.fs_manager.set_node_id(path, "uuid1")
4486@@ -117,7 +125,7 @@
4487 self.fs_manager.remove_partial("uuid1", "")
4488 d = self.tool.get_metadata(path)
4489 out = StringIO()
4490- out.encoding = 'utf-8'
4491+ out.encoding = encoding
4492 expected = """ File: %(path_info)s
4493 info_created: %(info_created)s
4494 info_is_partial: %(info_is_partial)s
4495@@ -136,7 +144,10 @@
4496 """
4497 # the callback, pylint: disable-msg=C0111
4498 def callback(result):
4499- result.update(dict(path_info=path.decode(out.encoding)))
4500+ if encoding is not None:
4501+ result.update(dict(path_info=path.decode(encoding)))
4502+ else:
4503+ result.update(dict(path_info=path))
4504 for k, v in result.copy().items():
4505 result[k] = v.decode('utf-8')
4506 value = expected % result
4507
4508=== modified file 'tests/syncdaemon/test_u1sdtool.pyc'
4509Binary files tests/syncdaemon/test_u1sdtool.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_u1sdtool.pyc 2010-03-31 23:55:45 +0000 differ
4510=== modified file 'tests/syncdaemon/test_vm.py'
4511--- tests/syncdaemon/test_vm.py 2010-03-04 16:47:43 +0000
4512+++ tests/syncdaemon/test_vm.py 2010-03-31 23:55:45 +0000
4513@@ -1819,7 +1819,8 @@
4514 for new_key in new_keys:
4515 self.assertIn(new_key, old_keys)
4516 # check the old data is still there (in the backup)
4517- backup_shelf = LegacyShareFileShelf(os.path.join(self.vm_data_dir, '0.bkp'))
4518+ bkp_dir = os.path.join(os.path.dirname(self.vm_data_dir), '5.bkp', '0.bkp')
4519+ backup_shelf = LegacyShareFileShelf(bkp_dir)
4520 backup_keys = [key for key in backup_shelf.keys()]
4521 for old_key in old_keys:
4522 self.assertIn(old_key, backup_keys)
4523@@ -2420,6 +2421,183 @@
4524 self.assertEquals(udf.suggested_path, old_udf.suggested_path)
4525 self.assertEquals(udf.subscribed, old_udf.subscribed)
4526
4527+ def test_upgrade_5_partial_upgrade(self):
4528+ """Test migration from version 5 with upgrade to 6 unfinished."""
4529+ # build a fake version 5 state
4530+ self._build_layout_version_4()
4531+ self.set_md_version('5')
4532+ self.udfs_md_dir = os.path.join(self.vm_data_dir, 'udfs')
4533+ # create some old shares and shared metadata
4534+ legacy_shares = LegacyShareFileShelf(self.share_md_dir)
4535+ root_share = _Share(path=self.root_dir, share_id='',
4536+ access_level='Modify', node_id=str(uuid.uuid4()))
4537+ legacy_shares[''] = root_share
4538+ for idx, name in enumerate(['share'] * 3):
4539+ sid = str(uuid.uuid4())
4540+ share_name = name + '_' + str(idx)
4541+ share = _Share(path=os.path.join(self.shares_dir, share_name),
4542+ share_id=sid, name=share_name,
4543+ node_id=str(uuid.uuid4()),
4544+ other_username='username'+str(idx),
4545+ other_visible_name='visible name ' + str(idx))
4546+ if idx == 0:
4547+ share.access_level = 'Modify'
4548+ legacy_shares[sid] = share
4549+ elif idx == 1:
4550+ share.access_level = 'View'
4551+ legacy_shares[sid] = share
4552+ else:
4553+ # add a 'new' Share dict to the shelf
4554+ share.access_level = 'Modify'
4555+ share = Share(path=share.path,
4556+ volume_id=share.id, name=share.name,
4557+ access_level=share.access_level,
4558+ other_username=share.other_username,
4559+ other_visible_name=share.other_visible_name,
4560+ node_id=share.subtree)
4561+ legacy_shares[sid] = share.__dict__
4562+
4563+ # create shared shares
4564+ legacy_shared = LegacyShareFileShelf(self.shared_md_dir)
4565+ for idx, name in enumerate(['dir'] * 3):
4566+ sid = str(uuid.uuid4())
4567+ share_name = name + '_' + str(idx)
4568+ share = _Share(path=os.path.join(self.root_dir, share_name),
4569+ share_id=sid, node_id=str(uuid.uuid4()),
4570+ name=share_name, other_username='hola',
4571+ other_visible_name='hola')
4572+ if idx == 0:
4573+ share.access_level = 'Modify'
4574+ legacy_shares[sid] = share
4575+ elif idx == 1:
4576+ share.access_level = 'View'
4577+ legacy_shares[sid] = share
4578+ else:
4579+ # add a 'new' Shared dict to the shelf
4580+ share.access_level = 'Modify'
4581+ share = Shared(path=share.path,
4582+ volume_id=share.id, name=share.name,
4583+ access_level=share.access_level,
4584+ other_username=share.other_username,
4585+ other_visible_name=share.other_visible_name,
4586+ node_id=share.subtree)
4587+ legacy_shares[sid] = share.__dict__
4588+
4589+ # keep a copy of the current shares and shared metadata to check
4590+ # the upgrade went ok
4591+ legacy_shares = dict(legacy_shares.items())
4592+ legacy_shared = dict(legacy_shared.items())
4593+
4594+ if self.md_version_None:
4595+ self.set_md_version('')
4596+ # upgrade it!
4597+ self.main = FakeMain(self.root_dir, self.shares_dir,
4598+ self.data_dir, self.partials_dir)
4599+ vm = self.main.vm
4600+ def compare_share(share, old_share):
4601+ """Compare two shares, new and old"""
4602+ old_id = getattr(old_share, 'id', None)
4603+ if old_id is None:
4604+ old_id = old_share['volume_id']
4605+ self.assertEquals(share.volume_id, old_id)
4606+ self.assertEquals(share.path,
4607+ getattr(old_share, 'path', None) or old_share['path'])
4608+ self.assertEquals(share.node_id,
4609+ getattr(old_share, 'subtree', None) or old_share['node_id'])
4610+ if not isinstance(share, Root):
4611+ self.assertEquals(share.name,
4612+ getattr(old_share, 'name', None) or old_share['name'])
4613+ self.assertEquals(share.other_username,
4614+ getattr(old_share, 'other_username', None) \
4615+ or old_share['other_username'])
4616+ self.assertEquals(share.other_visible_name,
4617+ getattr(old_share, 'other_visible_name', None) \
4618+ or old_share['other_visible_name'])
4619+ self.assertEquals(share.access_level,
4620+ getattr(old_share, 'access_level', None) \
4621+ or old_share['access_level'])
4622+
4623+ for sid in vm.shares:
4624+ old_share = legacy_shares[sid]
4625+ share = vm.shares[sid]
4626+ self.assertTrue(isinstance(share, Share) or isinstance(share, Root))
4627+ compare_share(share, old_share)
4628+
4629+ for sid in vm.shared:
4630+ old_share = legacy_shared[sid]
4631+ share = vm.shared[sid]
4632+ self.assertTrue(isinstance(share, Shared))
4633+ compare_share(share, old_share)
4634+
4635+ def test_upgrade_5_critical_error(self):
4636+ """Test the migration from version 5 with a critical error."""
4637+ # build a fake version 5 state
4638+ self._build_layout_version_4()
4639+ self.set_md_version('5')
4640+ # create some old shares and shared metadata
4641+ legacy_shares = LegacyShareFileShelf(self.share_md_dir)
4642+ root_share = _Share(path=self.root_dir, share_id='',
4643+ access_level='Modify')
4644+ legacy_shares[''] = root_share
4645+ for idx, name in enumerate(['share'] * 10):
4646+ sid = str(uuid.uuid4())
4647+ share_name = name + '_' + str(idx)
4648+ share = _Share(path=os.path.join(self.shares_dir, share_name),
4649+ share_id=sid, name=share_name,
4650+ node_id=str(uuid.uuid4()),
4651+ other_username='username'+str(idx),
4652+ other_visible_name='visible name ' + str(idx))
4653+ if idx % 2:
4654+ share.access_level = 'Modify'
4655+ else:
4656+ share.access_level = 'View'
4657+ legacy_shares[sid] = share
4658+ # create shared shares
4659+ legacy_shared = LegacyShareFileShelf(self.shared_md_dir)
4660+ for idx, name in enumerate(['dir'] * 5):
4661+ sid = str(uuid.uuid4())
4662+ share_name = name + '_' + str(idx)
4663+ share = _Share(path=os.path.join(self.root_dir, share_name),
4664+ share_id=sid, node_id=str(uuid.uuid4()),
4665+ name=share_name, other_username='hola',
4666+ other_visible_name='hola')
4667+ if idx % 2:
4668+ share.access_level = 'Modify'
4669+ else:
4670+ share.access_level = 'View'
4671+ legacy_shared[sid] = share
4672+
4673+ # keep a copy of the current shares and shared metadata to check
4674+ # the upgrade went ok
4675+ legacy_shares = dict(legacy_shares.items())
4676+ legacy_shared = dict(legacy_shared.items())
4677+
4678+ if self.md_version_None:
4679+ self.set_md_version('')
4680+ # upgrade it!
4681+ old_upgrade_share_to_volume = MetadataUpgrader._upgrade_share_to_volume
4682+ def upgrade_share_to_volume(share, shared=False):
4683+ raise ValueError('FAIL!')
4684+ MetadataUpgrader._upgrade_share_to_volume = upgrade_share_to_volume
4685+ try:
4686+ self.assertRaises(ValueError, FakeMain, self.root_dir, self.shares_dir,
4687+ self.data_dir, self.partials_dir)
4688+ finally:
4689+ MetadataUpgrader._upgrade_share_to_volume = old_upgrade_share_to_volume
4690+
4691+ shares = LegacyShareFileShelf(self.share_md_dir)
4692+ self.assertEquals(len(list(shares.keys())), len(legacy_shares.keys()))
4693+ for sid, share in shares.iteritems():
4694+ old_share = legacy_shares[sid]
4695+ self.assertTrue(isinstance(share, _Share))
4696+ self.assertTrue(isinstance(old_share, _Share))
4697+ shared = LegacyShareFileShelf(self.shared_md_dir)
4698+ self.assertEquals(len(list(shared.keys())), len(legacy_shared.keys()))
4699+ for sid, share in shared.iteritems():
4700+ old_share = legacy_shared[sid]
4701+ self.assertTrue(isinstance(share, _Share))
4702+ self.assertTrue(isinstance(old_share, _Share))
4703+
4704
4705 class BrokenOldMDVersionUpgradeTests(MetadataOldLayoutTests):
4706 """MetadataOldLayoutTests with broken .version file."""
4707@@ -2520,3 +2698,11 @@
4708 version = self.md_upgrader._guess_metadata_version()
4709 self.assertEquals(version, '6')
4710
4711+ def test_guess_mixed_metadata_5_and_6(self):
4712+ """Test _guess_metadata_version method for mixed version 5 and 6."""
4713+ # fake a version 6 layout and metadata
4714+ shelf = LegacyShareFileShelf(self.share_md_dir)
4715+ shelf['old_share'] = _Share(path='/foo/bar', share_id='old_share')
4716+ shelf['new_share'] = Share(path='/bar/foo', volume_id='new_share').__dict__
4717+ version = self.md_upgrader._guess_metadata_version()
4718+ self.assertEquals(version, '5')
4719
4720=== modified file 'tests/syncdaemon/test_vm.pyc'
4721Binary files tests/syncdaemon/test_vm.pyc 2010-03-10 23:36:53 +0000 and tests/syncdaemon/test_vm.pyc 2010-03-31 23:55:45 +0000 differ
4722=== modified file 'tests/test_login.pyc'
4723Binary files tests/test_login.pyc 2010-03-10 23:36:53 +0000 and tests/test_login.pyc 2010-03-31 23:55:45 +0000 differ
4724=== modified file 'tests/test_preferences.py'
4725--- tests/test_preferences.py 2010-03-10 23:36:53 +0000
4726+++ tests/test_preferences.py 2010-03-31 23:55:45 +0000
4727@@ -21,7 +21,7 @@
4728 import os
4729 import gnomekeyring
4730
4731-from contrib.mocker import MockerTestCase, KWARGS
4732+from contrib.mocker import MockerTestCase
4733 from contrib.testing.testcase import DBusTwistedTestCase, FakeLogin
4734 from twisted.internet import defer
4735 from twisted.python.failure import Failure
4736@@ -48,7 +48,6 @@
4737
4738 # For testing keyring queries
4739 self.keyring = self.mocker.mock()
4740- self.u1prefs.httplib2 = self.mocker.mock()
4741 self.item = self.mocker.mock(gnomekeyring.Found)
4742
4743 self.item_id = 999
4744@@ -72,6 +71,8 @@
4745 self.mocker.count(0, None)
4746 self.mocker.result(None)
4747
4748+ self.u1prefs.make_rest_request = self.make_rest_request
4749+
4750 def tearDown(self):
4751 # collect all signal receivers registered during the test
4752 signal_receivers = set()
4753@@ -88,10 +89,19 @@
4754 d.addBoth(lambda _: DBusTwistedTestCase.tearDown(self))
4755 return d
4756
4757+ def make_rest_request(self, url=None, method='GET',
4758+ callback=None, keyring=None):
4759+ """Override the real request call to mock some stuff."""
4760+ if callback:
4761+ callback(self.content)
4762+ else:
4763+ raise Exception('No callback provided.')
4764+
4765 def test_bw_throttling(self):
4766 """Test that toggling bw throttling works correctly."""
4767 self.mocker.replay()
4768 widget = self.u1prefs.DevicesWidget(None, keyring=self.keyring)
4769+ widget.update_bw_settings = self.mocker.mock()
4770 try:
4771 widget.devices = []
4772 widget.list_devices()
4773@@ -116,6 +126,7 @@
4774 def test_list_devices_fills_devices_list_with_fake_result_when_empty(self):
4775 self.mocker.replay()
4776 widget = self.u1prefs.DevicesWidget(None, keyring=self.keyring)
4777+ widget.update_bw_settings = self.mocker.mock()
4778 try:
4779 widget.devices = []
4780 widget.list_devices()
4781@@ -129,6 +140,7 @@
4782 def test_list_devices_shows_devices_list(self):
4783 self.mocker.replay()
4784 widget = self.u1prefs.DevicesWidget(None, keyring=self.keyring)
4785+ widget.update_bw_settings = self.mocker.mock()
4786 try:
4787 widget.devices = []
4788 widget.list_devices()
4789@@ -159,6 +171,7 @@
4790 def test_list_devices_shows_real_devices_list(self):
4791 self.mocker.replay()
4792 widget = self.u1prefs.DevicesWidget(None, keyring=self.keyring)
4793+ widget.update_bw_settings = self.mocker.mock()
4794 try:
4795 widget.devices = [{'kind': 'Computer',
4796 'description': 'xyzzy',
4797@@ -203,12 +216,7 @@
4798
4799 def test_request_quota_info(self):
4800 """Test that we can request the quota info properly."""
4801- response = { 'status' : '200' }
4802- content = '{"total":2048, "used":1024}'
4803- client = self.mocker.mock()
4804- self.expect(self.u1prefs.httplib2.Http()).result(client)
4805- self.expect(client.request('https://one.ubuntu.com/api/quota/',
4806- 'GET', KWARGS)).result((response, content))
4807+ self.content = {"total":2048, "used":1024}
4808 self.mocker.replay()
4809 dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
4810 self.assertTrue(dialog is not None)
4811@@ -217,23 +225,116 @@
4812 self.assertEqual(dialog.usage_graph.get_fraction(), 0.5)
4813 dialog.destroy()
4814
4815+ def test_no_overquota_notice_on_low_usage(self):
4816+ """Test that the quota notice is not visible if usage is low."""
4817+ self.content = {"total":2048, "used":1024}
4818+ self.mocker.replay()
4819+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
4820+ self.assertTrue(dialog is not None)
4821+ dialog.request_quota_info()
4822+ # the label should just be the blank '\n'
4823+ self.assertEqual(dialog.overquota_label.get_text(), '\n')
4824+ # and the icon should be blank, too
4825+ self.assertEqual(dialog.overquota_img.get_icon_name()[0], None)
4826+ dialog.destroy()
4827+
4828+ def test_overquota_notice_and_upgrade_offer_on_free_high_usage(self):
4829+ """Test that the quota notice is visible if usage is 95%.
4830+
4831+ Also make sure the label mentions upgrading.
4832+ """
4833+ self.content = {"total": 100, "used": 95}
4834+ self.mocker.replay()
4835+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
4836+ self.assertTrue(dialog is not None)
4837+ dialog.request_quota_info()
4838+ # don't check the exact text, as it will probably
4839+ # change. Check we're setting some amount of text,
4840+ self.assertTrue(len(dialog.overquota_label.get_text()) > 100)
4841+ # and check that we mention upgrading
4842+ self.assertTrue('upgrad' in dialog.overquota_label.get_text().lower())
4843+ # and check that the icon used is just informational
4844+ self.assertEqual(dialog.overquota_img.get_icon_name()[0],
4845+ 'dialog-information')
4846+ dialog.destroy()
4847+
4848+ def test_overquota_notice_and_not_upgrade_offer_on_paid_high_usage(self):
4849+ """Test that the quota notice is visible if usage is 95%.
4850+
4851+ Also make sure the label does not mention upgrading.
4852+ """
4853+ self.content = {"total": 50<<30, "used": 49<<30}
4854+ self.mocker.replay()
4855+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
4856+ self.assertTrue(dialog is not None)
4857+ dialog.request_quota_info()
4858+ # don't check the exact text, as it will probably
4859+ # change. Check we're setting some amount of text,
4860+ self.assertTrue(len(dialog.overquota_label.get_text()) > 100)
4861+ # and check that we mention upgrading
4862+ self.assertTrue('upgrad' not in
4863+ dialog.overquota_label.get_text().lower())
4864+ # and check that the icon used is just informational
4865+ self.assertEqual(dialog.overquota_img.get_icon_name()[0],
4866+ 'dialog-information')
4867+ dialog.destroy()
4868+
4869+ def test_overquota_warning_and_upgrade_offer_there_on_free_over_quota(self):
4870+ """Test that the quota notice is visible if usage is 100%.
4871+
4872+ Also make sure the label mentions upgrading.
4873+ """
4874+ self.content = {"total": 100, "used": 100}
4875+ self.mocker.replay()
4876+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
4877+ self.assertTrue(dialog is not None)
4878+ dialog.request_quota_info()
4879+ # don't check the exact text, as it will probably
4880+ # change. Check we're setting some amount of text,
4881+ self.assertTrue(len(dialog.overquota_label.get_text()) > 100)
4882+ # and check that we mention upgrading
4883+ self.assertTrue('upgrad' in dialog.overquota_label.get_text().lower())
4884+ # and check that the icon used is a warning
4885+ self.assertEqual(dialog.overquota_img.get_icon_name()[0],
4886+ 'dialog-warning')
4887+ dialog.destroy()
4888+
4889+ def test_overquota_warning_there_on_paid_over_quota(self):
4890+ """Test that the quota notice is visible if usage is 100%."""
4891+ self.content = {"total": 50<<30, "used": 50<<30}
4892+ self.mocker.replay()
4893+ dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
4894+ self.assertTrue(dialog is not None)
4895+ dialog.request_quota_info()
4896+ # don't check the exact text, as it will probably
4897+ # change. Check we're setting some amount of text,
4898+ self.assertTrue(len(dialog.overquota_label.get_text()) > 100)
4899+ # and check that the icon used is a warning
4900+ self.assertEqual(dialog.overquota_img.get_icon_name()[0],
4901+ 'dialog-warning')
4902+ dialog.destroy()
4903+
4904 def test_request_account_info(self):
4905 """Test that we can request the account info properly."""
4906- response = { 'status' : '200' }
4907- content = '''{"username": "ubuntuone", "nickname": "Ubuntu One",
4908- "email": "uone@example.com"}
4909- '''
4910- client = self.mocker.mock()
4911- self.expect(self.u1prefs.httplib2.Http()).result(client)
4912- self.expect(client.request('https://one.ubuntu.com/api/account/',
4913- 'GET', KWARGS)).result((response, content))
4914+ self.content = {"username": "ubuntuone", "nickname": "Ubuntu One",
4915+ "email": "uone@example.com"}
4916 self.mocker.replay()
4917 dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
4918 self.assertTrue(dialog is not None)
4919 dialog.request_account_info()
4920+ self.content = {"total":2048, "used":1024}
4921+ dialog.request_quota_info()
4922 self.assertEqual(dialog.name_label.get_text(), 'Ubuntu One')
4923- self.assertEqual(dialog.user_label.get_text(), 'ubuntuone')
4924 self.assertEqual(dialog.mail_label.get_text(), 'uone@example.com')
4925+ # ensure the plan label says "Free" and the upgrade button is there
4926+ self.assertEqual(dialog.plan_label.get_text(), 'Free')
4927+ self.assertTrue(dialog.upgrade_link.get_visible())
4928+ # whoops, the user upgraded
4929+ self.content = {"total": 50<<30, "used":1024}
4930+ dialog.request_quota_info()
4931+ # ensure the plan label says "Paid" and the upgrade button is not there
4932+ self.assertEqual(dialog.plan_label.get_text(), 'Paid')
4933+ self.assertFalse(dialog.upgrade_link.get_visible())
4934 dialog.destroy()
4935
4936 def test_toggle_bookmarks(self):
4937@@ -287,6 +388,20 @@
4938 self.mocker.replay()
4939 dialog = self.u1prefs.UbuntuOneDialog(keyring=self.keyring)
4940 self.assertTrue(dialog is not None)
4941+ def files_toggled(checkbutton):
4942+ enabled = checkbutton.get_active()
4943+ if enabled:
4944+ dialog.music_check.set_sensitive(True)
4945+ else:
4946+ dialog.music_check.set_sensitive(False)
4947+
4948+ def music_toggled(checkbutton):
4949+ pass
4950+
4951+ dialog.files_check_toggled = files_toggled
4952+ dialog.music_check_toggled = music_toggled
4953+ dialog.connect_file_sync_callbacks()
4954+
4955 dialog.files_check.set_active(False)
4956 self.assertFalse(dialog.files_check.get_active())
4957 self.assertFalse(dialog.music_check.props.sensitive)
4958@@ -324,12 +439,13 @@
4959
4960 d = defer.Deferred()
4961
4962- login = self.u1prefs.UbuntuoneLoginHandler()
4963+ login = self.u1prefs.UbuntuoneLoginHandler(dialog=None)
4964 login.got_newcredentials = got_new_creds
4965 login.got_authdenied = got_auth_denied
4966 login.got_oautherror = got_oauth_error
4967 login.got_dbus_error = got_dbus_error
4968 login.register_signal_handlers()
4969- login.do_login_check()
4970+ self.u1prefs.do_login_request(bus=self.bus,
4971+ error_handler=got_dbus_error)
4972
4973 return d
4974
4975=== modified file 'tests/test_preferences.pyc'
4976Binary files tests/test_preferences.pyc 2010-03-10 23:36:53 +0000 and tests/test_preferences.pyc 2010-03-31 23:55:45 +0000 differ
4977=== modified file 'tests/u1sync/__init__.pyc'
4978Binary files tests/u1sync/__init__.pyc 2010-03-10 23:36:53 +0000 and tests/u1sync/__init__.pyc 2010-03-31 23:55:45 +0000 differ
4979=== modified file 'tests/u1sync/test_init.pyc'
4980Binary files tests/u1sync/test_init.pyc 2010-03-10 23:36:53 +0000 and tests/u1sync/test_init.pyc 2010-03-31 23:55:45 +0000 differ
4981=== modified file 'tests/u1sync/test_merge.pyc'
4982Binary files tests/u1sync/test_merge.pyc 2010-03-10 23:36:53 +0000 and tests/u1sync/test_merge.pyc 2010-03-31 23:55:45 +0000 differ
4983=== modified file 'ubuntuone/oauthdesktop/key_acls.py'
4984--- ubuntuone/oauthdesktop/key_acls.py 2009-06-30 12:00:00 +0000
4985+++ ubuntuone/oauthdesktop/key_acls.py 2010-03-31 23:55:45 +0000
4986@@ -36,7 +36,7 @@
4987 if use_source_tree_folder:
4988 source_tree_folder = os.path.join(
4989 os.path.split(__file__)[0],
4990- "../../../oauth_registration.d")
4991+ "../../data/oauth_registration.d")
4992 if os.path.isdir(source_tree_folder):
4993 return os.path.join(source_tree_folder, "..")
4994
4995
4996=== modified file 'ubuntuone/oauthdesktop/main.py'
4997--- ubuntuone/oauthdesktop/main.py 2010-02-17 23:51:29 +0000
4998+++ ubuntuone/oauthdesktop/main.py 2010-03-31 23:55:45 +0000
4999@@ -25,13 +25,12 @@
5000 signal so they can retrieve the new token.
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: