Merge lp:~nataliabidart/magicicada-gui/remove-folders into lp:magicicada-gui

Proposed by Natalia Bidart
Status: Merged
Approved by: Natalia Bidart
Approved revision: 118
Merged at revision: 116
Proposed branch: lp:~nataliabidart/magicicada-gui/remove-folders
Merge into: lp:magicicada-gui
Diff against target: 465 lines (+245/-30)
7 files modified
data/ui/folders.ui (+10/-0)
magicicada/dbusiface.py (+1/-1)
magicicada/gui/gtk/listings.py (+64/-5)
magicicada/gui/gtk/tests/__init__.py (+2/-0)
magicicada/gui/gtk/tests/test_listings.py (+162/-18)
magicicada/logger.py (+2/-2)
magicicada/tests/test_dbusiface.py (+4/-4)
To merge this branch: bzr merge lp:~nataliabidart/magicicada-gui/remove-folders
Reviewer Review Type Date Requested Status
Facundo Batista Approve
Review via email: mp+84320@code.launchpad.net

Commit message

- Fix how folder op results are handled (LP: #899305).
- Allow folder removal (LP: #568173).

To post a comment you must log in.
Revision history for this message
Facundo Batista (facundo) wrote :

+1

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

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/ui/folders.ui'
2--- data/ui/folders.ui 2011-10-11 23:51:51 +0000
3+++ data/ui/folders.ui 2011-12-06 00:43:23 +0000
4@@ -11,6 +11,15 @@
5 <property name="use_stock">True</property>
6 <signal name="clicked" handler="on_add_folder_button_clicked" swapped="no"/>
7 </object>
8+ <object class="GtkButton" id="remove_folder_button">
9+ <property name="label">gtk-remove</property>
10+ <property name="visible">True</property>
11+ <property name="can_focus">True</property>
12+ <property name="receives_default">True</property>
13+ <property name="use_action_appearance">False</property>
14+ <property name="use_stock">True</property>
15+ <signal name="clicked" handler="on_remove_folder_button_clicked" swapped="no"/>
16+ </object>
17 <object class="GtkScrolledWindow" id="root">
18 <property name="visible">True</property>
19 <property name="can_focus">True</property>
20@@ -25,6 +34,7 @@
21 <property name="search_column">0</property>
22 <property name="enable_grid_lines">both</property>
23 <property name="enable_tree_lines">True</property>
24+ <signal name="cursor-changed" handler="on_view_cursor_changed" swapped="no"/>
25 <child>
26 <object class="GtkTreeViewColumn" id="folders_path">
27 <property name="resizable">True</property>
28
29=== modified file 'magicicada/dbusiface.py'
30--- magicicada/dbusiface.py 2011-11-19 17:14:50 +0000
31+++ magicicada/dbusiface.py 2011-12-06 00:43:23 +0000
32@@ -558,7 +558,7 @@
33 raise FolderOperationError(**kwargs)
34 else:
35 logger.debug("%s folder finished ok: %s", act_name, result)
36- folder = self._get_folder_data(result[0])
37+ folder = self._get_folder_data(result)
38 defer.returnValue(folder)
39
40 def create_folder(self, path):
41
42=== modified file 'magicicada/gui/gtk/listings.py'
43--- magicicada/gui/gtk/listings.py 2011-10-11 23:51:51 +0000
44+++ magicicada/gui/gtk/listings.py 2011-12-06 00:43:23 +0000
45@@ -33,6 +33,12 @@
46
47
48 ADD_NEW_FOLDER = _('Add a new folder')
49+ARE_YOU_SURE = _('Are you sure?')
50+ARE_YOU_SURE_REMOVE_FOLDER = _('Are you sure you want to stop syncing '
51+ 'the folder %s?')
52+ARE_YOU_SURE_REMOVE_FOLDER_SECONDARY_TEXT = _('You will not loose any local '
53+ ' data, but the contents of this folder will no longer be synced to/from '
54+ ' your cloud in any of your registered devices.')
55 ERROR_MESSAGE_MARKUP = '<span foreground="red" font_weight="bold">%s</span>'
56 ERROR_MESSAGE = _('Oops! Something went wrong%(details)s')
57
58@@ -60,6 +66,13 @@
59 gtk.Dialog.__init__(self, **_kwargs)
60 self.close_button = self.get_action_area().get_children()[0]
61
62+ kwargs = dict(parent=self,
63+ flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
64+ type=gtk.MESSAGE_QUESTION,
65+ buttons=gtk.BUTTONS_YES_NO)
66+ self.confirm_dialog = gtk.MessageDialog(**kwargs)
67+ self.confirm_dialog.set_title(ARE_YOU_SURE)
68+
69 self.warning_label = gtk.Label()
70 self.warning_label.set_selectable(True)
71 self.warning_label.set_line_wrap(True)
72@@ -142,6 +155,31 @@
73 msg = ERROR_MESSAGE % dict(details=' (%s)' % error)
74 self.warning_label.set_markup(ERROR_MESSAGE_MARKUP % msg)
75
76+ @defer.inlineCallbacks
77+ def call_async(self, operation, *args, **kwargs):
78+ """Call 'operation(*args, **kwargs)' while disabling the ui.
79+
80+ The result of calling 'operation' is returned.
81+
82+ All errors are catched and logged, and shown to the user as a red
83+ warning.
84+
85+ """
86+ self.warning_label.set_text('')
87+ result = None
88+ self.set_sensitive(False)
89+ logger.debug('call_async: executing %r with args %r and %r',
90+ operation.__name__, args, kwargs)
91+ try:
92+ result = yield operation(*args, **kwargs)
93+ except Exception, e: # pylint: disable=W0703
94+ logger.exception('call_async: %r failed with:',
95+ operation.__name__)
96+ self.on_error(e)
97+ self.load()
98+ self.set_sensitive(True)
99+ defer.returnValue(result)
100+
101
102 class FoldersDialog(ListingDialog):
103 """The list of operations over files/folders."""
104@@ -154,8 +192,10 @@
105
106 def __init__(self, *args, **kwargs):
107 super(FoldersDialog, self).__init__(*args, **kwargs)
108+ self.remove_folder_button.set_sensitive(False)
109 action_area = self.get_action_area()
110 action_area.pack_start(self.add_folder_button)
111+ action_area.pack_start(self.remove_folder_button)
112 action_area.reorder_child(self.close_button, 0)
113
114 kwargs = dict(title=ADD_NEW_FOLDER, parent=None,
115@@ -179,12 +219,31 @@
116 response = self.file_chooser.run()
117 self.file_chooser.hide()
118 if response == gtk.RESPONSE_ACCEPT:
119+ path = self.file_chooser.get_filename()
120 # validate path?
121- self.set_sensitive(False)
122- path = self.file_chooser.get_filename()
123- yield self.sd.create_folder(path)
124- self.load()
125- self.set_sensitive(True)
126+ yield self.call_async(self.sd.create_folder, path)
127+
128+ def on_view_cursor_changed(self, view=None):
129+ """Enable the remove button if there's a row selected."""
130+ selection = self.view.get_selection()
131+ can_remove = selection.count_selected_rows() > 0
132+ self.remove_folder_button.set_sensitive(can_remove)
133+
134+ @defer.inlineCallbacks
135+ def on_remove_folder_button_clicked(self, button, *args, **kwargs):
136+ """The remove_folder button was clicked."""
137+ selection = self.view.get_selection()
138+ model, tree_iter = selection.get_selected()
139+ path = model.get_value(tree_iter, 0)
140+ self.confirm_dialog.set_markup(ARE_YOU_SURE_REMOVE_FOLDER % path)
141+ msg = ARE_YOU_SURE_REMOVE_FOLDER_SECONDARY_TEXT
142+ self.confirm_dialog.format_secondary_text(msg)
143+ response = self.confirm_dialog.run()
144+ self.confirm_dialog.hide()
145+
146+ if response == gtk.RESPONSE_YES:
147+ volume_id = model.get_value(tree_iter, 2)
148+ yield self.call_async(self.sd.delete_folder, volume_id)
149
150
151 class SharesToMeDialog(ListingDialog):
152
153=== modified file 'magicicada/gui/gtk/tests/__init__.py'
154--- magicicada/gui/gtk/tests/__init__.py 2011-10-11 23:51:51 +0000
155+++ magicicada/gui/gtk/tests/__init__.py 2011-12-06 00:43:23 +0000
156@@ -206,8 +206,10 @@
157 store = None
158 ui_class = main.MagicicadaUI
159
160+ @defer.inlineCallbacks
161 def setUp(self):
162 """Init."""
163+ yield super(BaseTestCase, self).setUp()
164 self.patch(main.syncdaemon, 'SyncDaemon', FakedSyncdaemon)
165 self.ui = self.ui_class(**self.kwargs)
166
167
168=== modified file 'magicicada/gui/gtk/tests/test_listings.py'
169--- magicicada/gui/gtk/tests/test_listings.py 2011-10-11 23:51:51 +0000
170+++ magicicada/gui/gtk/tests/test_listings.py 2011-12-06 00:43:23 +0000
171@@ -24,6 +24,9 @@
172
173 from magicicada.gui.gtk.listings import (
174 ADD_NEW_FOLDER,
175+ ARE_YOU_SURE,
176+ ARE_YOU_SURE_REMOVE_FOLDER,
177+ ARE_YOU_SURE_REMOVE_FOLDER_SECONDARY_TEXT,
178 ERROR_MESSAGE,
179 ERROR_MESSAGE_MARKUP,
180 FoldersButton,
181@@ -132,9 +135,10 @@
182 items = [TestDataField(), TestDataField(), TestDataField()]
183 ui_class = ListingDialog
184
185+ @defer.inlineCallbacks
186 def setUp(self):
187- """Init."""
188 self.patch(gtk, 'FileChooserDialog', FakeDialog)
189+ self.patch(gtk, 'MessageDialog', FakeDialog)
190
191 if self.ui_class.data_fields is None:
192 fakes = (('attr0', lambda s: ''.join(reversed(s))),
193@@ -152,7 +156,7 @@
194 self.kwargs['builder'] = builder
195 self.addCleanup(self.kwargs.pop, 'builder')
196
197- super(ListingDialogTestCase, self).setUp()
198+ yield super(ListingDialogTestCase, self).setUp()
199
200 setattr(self.ui.sd, self.ui.sd_attr, self.items)
201 self.store = self.ui.store
202@@ -340,19 +344,83 @@
203 self.assertEqual(self.ui.view.get_selection().get_mode(),
204 gtk.SELECTION_SINGLE)
205
206- def test_on_error(self):
207- """On error, show a error dialog."""
208- self.patch(gtk, 'MessageDialog', FakeDialog)
209-
210- exc = TypeError('foo')
211- self.ui.on_error(exc)
212+ def test_on_error(self, exc=None):
213+ """On error 'exc', show a error dialog."""
214+ if exc is None:
215+ exc = TypeError('foo')
216+ self.ui.on_error(exc)
217
218 self.assertTrue(self.ui.warning_label.get_visible())
219- error_msg = ERROR_MESSAGE % dict(details=' (foo)')
220+ error_msg = ERROR_MESSAGE % dict(details=' (%s)' % exc)
221 self.assertEqual(self.ui.warning_label.get_text(), error_msg)
222 self.assertEqual(self.ui.warning_label.get_label(),
223 ERROR_MESSAGE_MARKUP % error_msg)
224
225+ def test_confirm_dialog(self):
226+ """The confirm dialog dialog is properly created."""
227+ self.assertIsInstance(self.ui.confirm_dialog, gtk.MessageDialog)
228+
229+ def test_confirm_dialog_creation_params(self):
230+ """The confirm dialog dialog is created with the expected params."""
231+ self.assertEqual(self.ui.confirm_dialog.args, ())
232+ kwargs = dict(parent=self.ui,
233+ flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
234+ type=gtk.MESSAGE_QUESTION,
235+ buttons=gtk.BUTTONS_YES_NO)
236+ self.assertEqual(self.ui.confirm_dialog.kwargs, kwargs)
237+
238+ def test_confirm_dialog_title(self):
239+ """The confirm dialog dialog title is correct."""
240+ self.assert_method_called(self.ui.confirm_dialog, 'set_title',
241+ ARE_YOU_SURE)
242+
243+ @defer.inlineCallbacks
244+ def test_call_async_disables_ui(self):
245+ """While calling an async operation, the UI is disabled."""
246+ self.patch(self.ui, 'load', self._set_called)
247+
248+ def check():
249+ """Check ui sensibility."""
250+ result = self.ui.is_sensitive()
251+ return defer.succeed(result)
252+
253+ self.assertTrue(self.ui.is_sensitive(),
254+ 'The ui must be enabled before calling the op.')
255+
256+ was_sensitive = yield self.ui.call_async(check)
257+
258+ self.assertFalse(was_sensitive,
259+ 'The ui must be disabled while calling the op.')
260+ self.assertTrue(self.ui.is_sensitive(),
261+ 'The ui must be enabled after calling the op.')
262+ self.test_warning_label() # the warning_label is cleared
263+ self.assertEqual(self._called, ((), {}), 'load was called')
264+
265+ @defer.inlineCallbacks
266+ def test_call_async_handles_errors(self):
267+ """While calling an async operation, errors are catched and logged."""
268+ self.patch(self.ui, 'load', self._set_called)
269+ msg = 'Crash boom bang'
270+ exc = AssertionError(msg)
271+
272+ def fail_zaraza():
273+ """Throw any error."""
274+ return defer.fail(exc)
275+
276+ result = yield self.ui.call_async(fail_zaraza)
277+
278+ self.assertEqual(result, None)
279+ self.assertTrue(self.memento.check_exception(exc.__class__, msg))
280+ self.assertTrue(self.memento.check_error(fail_zaraza.__name__))
281+ self.test_on_error(exc=exc)
282+ self.assertEqual(self._called, ((), {}), 'load was called')
283+
284+ @defer.inlineCallbacks
285+ def test_call_async_success_after_error(self):
286+ """Warning messages are cleared after an error."""
287+ yield self.ui.call_async(lambda: defer.fail(ValueError()))
288+ yield self.test_call_async_disables_ui()
289+
290
291 class FoldersDialogTestCase(ListingDialogTestCase):
292 """UI test cases for folders."""
293@@ -405,14 +473,15 @@
294 """When the add_folder_button is clicked, the backend is called."""
295 self.patch(self.ui, 'load', self._set_called)
296 self.patch(self.ui.file_chooser, 'response', gtk.RESPONSE_ACCEPT)
297+
298+ def check():
299+ """Perform a middle check."""
300+ self.assertFalse(self.ui.is_sensitive())
301+
302+ self.patch(self.ui.sd, 'create_folder', check)
303+
304 path = '~/foo/test/me'
305-
306- def check():
307- """Perform a middle check and return path."""
308- self.assertFalse(self.ui.is_sensitive())
309- return path
310-
311- self.patch(self.ui.file_chooser, 'get_filename', check)
312+ self.patch(self.ui.file_chooser, 'get_filename', lambda: path)
313
314 yield self.ui.add_folder_button.clicked()
315
316@@ -427,6 +496,81 @@
317 self.assertEqual(self.ui.sd.on_folder_op_error_callback,
318 self.ui.on_error)
319
320+ def test_remove_folder_button_disabled(self):
321+ """At startup, the remove button is disabled."""
322+ self.ui.load()
323+ self.assertFalse(self.ui.remove_folder_button.get_sensitive())
324+
325+
326+class FoldersDialogRemoveFolderTestCase(FoldersDialogTestCase):
327+ """Test case for folder removal."""
328+
329+ @defer.inlineCallbacks
330+ def setUp(self):
331+ yield super(FoldersDialogRemoveFolderTestCase, self).setUp()
332+ self.ui.load()
333+ idx = 0 # the testing folder
334+ self.ui.view.set_cursor(idx)
335+ _, _, self.path, _, self.volume_id = SAMPLE_FOLDERS[idx]
336+
337+ def test_remove_folder_button_disabled(self):
338+ """When there is no folder selected, the remove button is disabled."""
339+ self.ui.view.get_selection().unselect_path('0')
340+ self.ui.on_view_cursor_changed()
341+ self.assertFalse(self.ui.remove_folder_button.get_sensitive())
342+
343+ def test_remove_folder_button_enabled(self):
344+ """When there is a folder selected, the remove button is enabled."""
345+ self.assertTrue(self.ui.remove_folder_button.get_sensitive())
346+
347+ @defer.inlineCallbacks
348+ def test_on_remove_folder_button_clicked_confirm_dialog_is_shown(self):
349+ """On remove_folder_button clicked, the confirm_dialog is shown."""
350+ yield self.ui.remove_folder_button.clicked()
351+
352+ msg = ARE_YOU_SURE_REMOVE_FOLDER % self.path
353+ self.assert_method_called(self.ui.confirm_dialog, 'set_markup', msg)
354+ self.assert_method_called(self.ui.confirm_dialog,
355+ 'format_secondary_text',
356+ ARE_YOU_SURE_REMOVE_FOLDER_SECONDARY_TEXT)
357+ self.assert_method_called(self.ui.confirm_dialog, 'run')
358+ self.assert_method_called(self.ui.confirm_dialog, 'hide')
359+
360+ @defer.inlineCallbacks
361+ def test_on_remove_folder_button_clicked_closes_dialog(self):
362+ """If the user closes the dialog, nothing is done."""
363+ self.patch(self.ui.confirm_dialog, 'response',
364+ gtk.RESPONSE_DELETE_EVENT)
365+ yield self.ui.remove_folder_button.clicked()
366+
367+ self.assert_no_method_called(self.ui.sd)
368+ self.assertTrue(self.ui.is_sensitive())
369+
370+ @defer.inlineCallbacks
371+ def test_on_remove_folder_button_clicked_answers_no(self):
372+ """If the user answers no, nothing is done."""
373+ self.patch(self.ui.confirm_dialog, 'response', gtk.RESPONSE_NO)
374+ yield self.ui.remove_folder_button.clicked()
375+
376+ self.assert_no_method_called(self.ui.sd)
377+ self.assertTrue(self.ui.is_sensitive())
378+
379+ @defer.inlineCallbacks
380+ def test_on_remove_folder_button_clicked_answers_yes(self):
381+ """If the user answers yes, sd.delete_folder is called."""
382+ self.patch(self.ui.confirm_dialog, 'response', gtk.RESPONSE_YES)
383+
384+ def check():
385+ """Perform a middle check."""
386+ self.assertFalse(self.ui.is_sensitive())
387+
388+ self.patch(self.ui.sd, 'delete_folder', check)
389+
390+ yield self.ui.remove_folder_button.clicked()
391+
392+ self.assert_method_called(self.ui.sd, 'delete_folder', self.volume_id)
393+ self.assertTrue(self.ui.is_sensitive())
394+
395
396 class SharesToMeDialogTestCase(ListingDialogTestCase):
397 """UI test cases for shares_to_me."""
398@@ -466,10 +610,10 @@
399 stock_id = None
400 ui_class = ListingButton
401
402+ @defer.inlineCallbacks
403 def setUp(self):
404- """Init."""
405 self.patch(self.ui_class, 'dialog_class', FakeDialog)
406- super(ListingButtonTestCase, self).setUp()
407+ yield super(ListingButtonTestCase, self).setUp()
408
409 def test_label(self):
410 """The label is correct."""
411
412=== modified file 'magicicada/logger.py'
413--- magicicada/logger.py 2011-12-05 21:20:00 +0000
414+++ magicicada/logger.py 2011-12-06 00:43:23 +0000
415@@ -85,8 +85,8 @@
416 handler = CustomRotatingFH(logfile, maxBytes=1e6, backupCount=10)
417 logger.addHandler(handler)
418 assert len(logger.handlers) == 1
419- formatter = logging.Formatter("%(asctime)s %(name)-23s"
420- "%(levelname)-8s %(message)s")
421+ formatter = logging.Formatter("%(asctime)s - %(name)s - "
422+ "%(levelname)s - %(message)s")
423 handler.setFormatter(formatter)
424 logger.setLevel(logging.DEBUG)
425
426
427=== modified file 'magicicada/tests/test_dbusiface.py'
428--- magicicada/tests/test_dbusiface.py 2011-11-19 17:14:50 +0000
429+++ magicicada/tests/test_dbusiface.py 2011-12-06 00:43:23 +0000
430@@ -1341,7 +1341,7 @@
431 """Creating a folder finishes ok."""
432 d = dict(node_id='nid', path=u'pth', subscribed='True', generation='5',
433 suggested_path=u'sgp', type='UDF', volume_id='vid')
434- self.fake_sdt_response('create_folder', (d,))
435+ self.fake_sdt_response('create_folder', d)
436 result = yield self.dbus.create_folder('pth')
437 self.assertEqual(result.path, 'pth')
438 self.assertEqual(result.suggested_path, 'sgp')
439@@ -1375,7 +1375,7 @@
440 """Deleting a folder finishes ok."""
441 d = dict(node_id='nid', path=u'pth', subscribed='', generation='5',
442 suggested_path=u'sgp', type='UDF', volume_id='vid')
443- self.fake_sdt_response('delete_folder', (d,))
444+ self.fake_sdt_response('delete_folder', d)
445 result = yield self.dbus.delete_folder('vid')
446 self.assertEqual(result.path, 'pth')
447 self.assertEqual(result.suggested_path, 'sgp')
448@@ -1409,7 +1409,7 @@
449 """Subscribing a folder finishes ok."""
450 d = dict(node_id='nid', path=u'pth', subscribed='', generation='5',
451 suggested_path=u'sgp', type='UDF', volume_id='vid')
452- self.fake_sdt_response('subscribe_folder', (d,))
453+ self.fake_sdt_response('subscribe_folder', d)
454 result = yield self.dbus.subscribe_folder('vid')
455 self.assertEqual(result.path, 'pth')
456 self.assertEqual(result.suggested_path, 'sgp')
457@@ -1443,7 +1443,7 @@
458 """Subscribing a folder finishes ok."""
459 d = dict(node_id='nid', path=u'pth', subscribed='', generation='5',
460 suggested_path=u'sgp', type='UDF', volume_id='vid')
461- self.fake_sdt_response('unsubscribe_folder', (d,))
462+ self.fake_sdt_response('unsubscribe_folder', d)
463 result = yield self.dbus.unsubscribe_folder('vid')
464 self.assertEqual(result.path, 'pth')
465 self.assertEqual(result.suggested_path, 'sgp')

Subscribers

People subscribed via source and target branches

to all changes: