Merge lp:~oem-community-qa/checkbox-editor/bug619720 into lp:checkbox-editor
- bug619720
- Merge into trunk
Proposed by
Javier Collado
Status: | Merged |
---|---|
Approved by: | Javier Collado |
Approved revision: | 59 |
Merge reported by: | Javier Collado |
Merged at revision: | not available |
Proposed branch: | lp:~oem-community-qa/checkbox-editor/bug619720 |
Merge into: | lp:checkbox-editor |
Diff against target: |
927 lines (+378/-185) 9 files modified
checkbox_editor/editor.py (+107/-47) checkbox_editor/form.py (+6/-10) checkbox_editor/glade/editor.glade (+31/-2) checkbox_editor/model.py (+122/-43) checkbox_editor/preferences.py (+17/-31) checkbox_editor/treeview.py (+19/-34) checkbox_editor/util.py (+32/-0) checkbox_editor/vc.py (+37/-18) debian/changelog (+7/-0) |
To merge this branch: | bzr merge lp:~oem-community-qa/checkbox-editor/bug619720 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Javier Collado (community) | Approve | ||
Review via email: mp+34447@code.launchpad.net |
Commit message
Description of the change
Message dialog warns user on shared directory insufficient permissions
'Save as' functionality implemented
To post a comment you must log in.
Revision history for this message
Javier Collado (javier.collado) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'checkbox_editor/editor.py' |
2 | --- checkbox_editor/editor.py 2010-08-31 10:10:41 +0000 |
3 | +++ checkbox_editor/editor.py 2010-09-02 16:41:06 +0000 |
4 | @@ -17,7 +17,7 @@ |
5 | from .treeview import Treeview |
6 | from .form import Form |
7 | from .statusbar import StatusBar |
8 | -from .util import CommandLauncher, ExceptionHandled, exception_handler |
9 | +from .util import CommandLauncher, ExceptionHandled, exception_handler, MessageDialogRunner |
10 | from . import build |
11 | |
12 | |
13 | @@ -63,6 +63,7 @@ |
14 | 'close_menuitem_activate_cb': self.close_menuitem_activate_cb, |
15 | 'revert_menuitem_activate_cb': self.revert_menuitem_activate_cb, |
16 | 'save_menuitem_activate_cb': self.save_menuitem_activate_cb, |
17 | + 'save_as_menuitem_activate_cb': self.save_as_menuitem_activate_cb, |
18 | 'open_testplan_menuitem_activate_cb': self.open_testplan_menuitem_activate_cb, |
19 | 'save_testplan_menuitem_activate_cb': self.save_testplan_menuitem_activate_cb, |
20 | 'save_testplan_as_menuitem_activate_cb': self.save_testplan_as_menuitem_activate_cb, |
21 | @@ -109,6 +110,7 @@ |
22 | for widget_name in widget_names: |
23 | self.builder.get_object(widget_name).set_sensitive(True) |
24 | |
25 | + self.set_title(options.directory) |
26 | self.statusbar._write_tmp_message('Shared directory opened') |
27 | |
28 | treeview = self.builder.get_object('treeview') |
29 | @@ -173,19 +175,20 @@ |
30 | window.add_accel_group(group) |
31 | |
32 | accelerators_data = [ |
33 | - ('open_menuitem', 'o'), |
34 | - ('close_menuitem', 'w'), |
35 | - ('quit_menuitem', 'q'), |
36 | - ('save_menuitem', 's'), |
37 | - ('add_menuitem', 'a'), |
38 | - ('remove_menuitem', 'r'), |
39 | - ('preferences_menuitem', 'p'), |
40 | - ('contents_menuitem', 'F1'), |
41 | + ('open_menuitem', '<Control>o'), |
42 | + ('close_menuitem', '<Control>w'), |
43 | + ('quit_menuitem', '<Control>q'), |
44 | + ('save_menuitem', '<Control>s'), |
45 | + ('save_as_menuitem', '<Shift><Control>s'), |
46 | + ('add_menuitem', '<Control>a'), |
47 | + ('remove_menuitem', '<Control>r'), |
48 | + ('preferences_menuitem', '<Control>p'), |
49 | + ('contents_menuitem', '<Control>F1'), |
50 | ] |
51 | |
52 | - for widget_name, key in accelerators_data: |
53 | + for widget_name, keys in accelerators_data: |
54 | menuitem = self.builder.get_object(widget_name) |
55 | - accel_key, accel_mod = gtk.accelerator_parse("<Control>%s" % key) |
56 | + accel_key, accel_mod = gtk.accelerator_parse(keys) |
57 | menuitem.add_accelerator("activate", |
58 | group, |
59 | accel_key, |
60 | @@ -238,6 +241,19 @@ |
61 | return options |
62 | |
63 | |
64 | + def set_title(self, directory=None): |
65 | + """ |
66 | + Set main window title based on the opened directory |
67 | + """ |
68 | + title = 'Checkbox Editor' |
69 | + if directory: |
70 | + title = '{0} - '.format(directory) + title |
71 | + if not os.access(directory, os.F_OK | os.W_OK): |
72 | + title = '[read only] ' + title |
73 | + window = self.builder.get_object('window') |
74 | + window.set_title(title) |
75 | + |
76 | + |
77 | def window_delete_event_cb(self, window, event): |
78 | """ |
79 | Delete main window |
80 | @@ -294,23 +310,26 @@ |
81 | directory = dialog.get_filename() |
82 | dialog.destroy() |
83 | |
84 | - if response == gtk.RESPONSE_ACCEPT: |
85 | - if model.is_valid_directory(directory): |
86 | - self.preferences.load(directory) |
87 | - if ExceptionHandled == model.load(directory): |
88 | - model.clear() |
89 | - else: |
90 | - for widget_name in ('close_menuitem', 'close_toolbutton', |
91 | - 'open_testplan_menuitem'): |
92 | - widget = self.builder.get_object(widget_name) |
93 | - widget.set_sensitive(True) |
94 | - |
95 | - for widget_name in ('push_menuitem', 'push_toolbutton', |
96 | - 'commit_menuitem', 'commit_toolbutton'): |
97 | - widget = self.builder.get_object(widget_name) |
98 | - widget.set_sensitive(False) |
99 | - |
100 | - self.statusbar._write_tmp_message('Shared directory opened') |
101 | + if response != gtk.RESPONSE_ACCEPT: |
102 | + return False |
103 | + |
104 | + if model.is_valid_directory(directory): |
105 | + self.preferences.load(directory) |
106 | + if ExceptionHandled == model.load(directory): |
107 | + model.clear() |
108 | + else: |
109 | + for widget_name in ('close_menuitem', 'close_toolbutton', |
110 | + 'open_testplan_menuitem'): |
111 | + widget = self.builder.get_object(widget_name) |
112 | + widget.set_sensitive(True) |
113 | + |
114 | + for widget_name in ('push_menuitem', 'push_toolbutton', |
115 | + 'commit_menuitem', 'commit_toolbutton'): |
116 | + widget = self.builder.get_object(widget_name) |
117 | + widget.set_sensitive(False) |
118 | + |
119 | + self.set_title(directory) |
120 | + self.statusbar._write_tmp_message('Shared directory opened') |
121 | |
122 | return False |
123 | |
124 | @@ -322,7 +341,6 @@ |
125 | if not self._continue_without_saving(): |
126 | return False |
127 | |
128 | - |
129 | treeview = self.builder.get_object('treeview') |
130 | model = treeview.get_model() |
131 | model.clear() |
132 | @@ -332,13 +350,12 @@ |
133 | self.preferences.load() |
134 | |
135 | widget_names = ['close_menuitem', 'close_toolbutton', |
136 | - 'save_menuitem', 'save_toolbutton', |
137 | - 'revert_menuitem', 'revert_toolbutton', |
138 | 'open_testplan_menuitem', 'save_testplan_menuitem', |
139 | 'save_testplan_as_menuitem', 'export_testplan_menuitem'] |
140 | for widget_name in widget_names: |
141 | self.builder.get_object(widget_name).set_sensitive(False) |
142 | |
143 | + self.set_title() |
144 | self.statusbar._write_tmp_message('Shared directory closed') |
145 | |
146 | return False |
147 | @@ -354,6 +371,54 @@ |
148 | if saved is True: |
149 | self.statusbar._write_tmp_message('Changes saved') |
150 | |
151 | + return False |
152 | + |
153 | + |
154 | + def save_as_menuitem_activate_cb(self, widget): |
155 | + """ |
156 | + Save changes to a different directory |
157 | + """ |
158 | + # Choose a destination directory |
159 | + dialog = gtk.FileChooserDialog('Choose Destination Shared Directory', |
160 | + buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, |
161 | + gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT), |
162 | + action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) |
163 | + |
164 | + while True: |
165 | + response = dialog.run() |
166 | + directory = dialog.get_filename() |
167 | + |
168 | + if response != gtk.RESPONSE_ACCEPT: |
169 | + dialog.destroy() |
170 | + return False |
171 | + |
172 | + if not os.path.isdir(directory): |
173 | + message = ("Selected directory doesn't seem to be a valid " |
174 | + 'directory:\n{0}\n\n' |
175 | + 'Please make sure that the path <b>exists</b> ' |
176 | + "and that it's a directory" |
177 | + .format(directory)) |
178 | + MessageDialogRunner('Invalid directory', message).run() |
179 | + |
180 | + elif not os.access(directory, os.F_OK | os.R_OK | os.W_OK | os.X_OK): |
181 | + message = ('Not enough <b>permissions</b> ' |
182 | + 'to read/write/execute selected directory:\n{0}\n\n' |
183 | + .format(directory)) |
184 | + MessageDialogRunner('Insuficcient permissions', message).run() |
185 | + |
186 | + elif bool(os.listdir(directory)): |
187 | + message = ("Selected directory isn't <b>empty</b>:\n{0}\n\n" |
188 | + 'Please select an empty directory, ' |
189 | + 'so that it can be populated with new data properly' |
190 | + .format(directory)) |
191 | + MessageDialogRunner('Not empty directory', message).run() |
192 | + else: |
193 | + break |
194 | + dialog.destroy() |
195 | + |
196 | + model = self.builder.get_object('treeview').get_model() |
197 | + model.save_as(directory) |
198 | + self.set_title(directory) |
199 | |
200 | return False |
201 | |
202 | @@ -363,18 +428,16 @@ |
203 | Check if there are pending changes and ask user |
204 | if he wants to continue |
205 | """ |
206 | - # If save menuitem is sensitive, |
207 | + # If save_as_menuitem is sensitive, |
208 | # it means there are pending (not saved) changes |
209 | - if self.builder.get_object('save_menuitem').get_property('sensitive'): |
210 | - message = ('Changes have not been saved.\n\n' |
211 | + if self.builder.get_object('save_as_menuitem').get_property('sensitive'): |
212 | + message = ('Changes have <b>not</b> been saved.\n\n' |
213 | 'Would you like to continue?') |
214 | - dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING, |
215 | - buttons=gtk.BUTTONS_YES_NO, |
216 | - message_format=message) |
217 | - response = dialog.run() |
218 | - dialog.destroy() |
219 | + response = MessageDialogRunner('Changes not saved', message, |
220 | + type=gtk.MESSAGE_WARNING, |
221 | + buttons=gtk.BUTTONS_YES_NO).run() |
222 | |
223 | - if response == gtk.RESPONSE_NO: |
224 | + if response != gtk.RESPONSE_YES: |
225 | return False |
226 | |
227 | return True |
228 | @@ -557,13 +620,10 @@ |
229 | 'Please note that they are not going to be available\n' |
230 | 'in checkbox.\n\n' |
231 | 'Would you like to continue?') |
232 | - dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL, |
233 | - type=gtk.MESSAGE_WARNING, |
234 | - buttons=gtk.BUTTONS_YES_NO) |
235 | - dialog.set_markup(message) |
236 | - response = dialog.run() |
237 | - dialog.destroy() |
238 | - if response == gtk.RESPONSE_NO: |
239 | + response = MessageDialogRunner('Changes not saved', message, |
240 | + type=gtk.MESSAGE_WARNING, |
241 | + buttons=gtk.BUTTONS_YES_NO).run() |
242 | + if response != gtk.RESPONSE_YES: |
243 | return |
244 | |
245 | treeview = self.builder.get_object('treeview') |
246 | |
247 | === modified file 'checkbox_editor/form.py' |
248 | --- checkbox_editor/form.py 2010-08-31 09:13:14 +0000 |
249 | +++ checkbox_editor/form.py 2010-09-02 16:41:06 +0000 |
250 | @@ -8,7 +8,7 @@ |
251 | import os, re, logging, shlex |
252 | |
253 | from .model import TreeStore |
254 | -from .util import CommandLauncher, SignalBlocker |
255 | +from .util import CommandLauncher, SignalBlocker, MessageDialogRunner |
256 | |
257 | class Form(object): |
258 | """ |
259 | @@ -374,16 +374,12 @@ |
260 | description = row[TreeStore.DESCRIPTION] |
261 | |
262 | # Prompt user to confirm change |
263 | - message=('Jobs that use the {0} plugin cannot contain ' |
264 | + message=('Jobs that use the {0!r} plugin cannot contain ' |
265 | 'children jobs definitions, so they will <b>removed</b>. ' |
266 | - 'Is that what you would like to do?'.format(repr(value))) |
267 | - dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING, |
268 | - buttons=gtk.BUTTONS_YES_NO) |
269 | - dialog.set_title('Cannot have children') |
270 | - dialog.set_markup(message) |
271 | - response = dialog.run() |
272 | - dialog.destroy() |
273 | - |
274 | + 'Is that what you would like to do?'.format(value)) |
275 | + response = MessageDialogRunner('Cannot have children', message, |
276 | + type=gtk.MESSAGE_WARNING, |
277 | + buttons=gtk.BUTTONS_YES_NO).run() |
278 | change_cancelled = False if response == gtk.RESPONSE_YES else True |
279 | |
280 | if change_cancelled: |
281 | |
282 | === modified file 'checkbox_editor/glade/editor.glade' |
283 | --- checkbox_editor/glade/editor.glade 2010-08-31 09:13:14 +0000 |
284 | +++ checkbox_editor/glade/editor.glade 2010-09-02 16:41:06 +0000 |
285 | @@ -76,6 +76,16 @@ |
286 | </object> |
287 | </child> |
288 | <child> |
289 | + <object class="GtkImageMenuItem" id="save_as_menuitem"> |
290 | + <property name="label" translatable="yes">Save as...</property> |
291 | + <property name="visible">True</property> |
292 | + <property name="sensitive">False</property> |
293 | + <property name="image">save_as_image_1</property> |
294 | + <property name="use_stock">False</property> |
295 | + <signal name="activate" handler="save_as_menuitem_activate_cb"/> |
296 | + </object> |
297 | + </child> |
298 | + <child> |
299 | <object class="GtkImageMenuItem" id="revert_menuitem"> |
300 | <property name="label">gtk-revert-to-saved</property> |
301 | <property name="visible">True</property> |
302 | @@ -115,7 +125,7 @@ |
303 | <property name="label" translatable="yes">Save test plan as...</property> |
304 | <property name="visible">True</property> |
305 | <property name="sensitive">False</property> |
306 | - <property name="image">save_as_image</property> |
307 | + <property name="image">save_as_image_2</property> |
308 | <property name="use_stock">False</property> |
309 | <signal name="activate" handler="save_testplan_as_menuitem_activate_cb"/> |
310 | </object> |
311 | @@ -354,6 +364,21 @@ |
312 | </packing> |
313 | </child> |
314 | <child> |
315 | + <object class="GtkToolButton" id="save_as_toolbutton"> |
316 | + <property name="visible">True</property> |
317 | + <property name="sensitive">False</property> |
318 | + <property name="tooltip_text" translatable="yes">Save changes to a different directory</property> |
319 | + <property name="label" translatable="yes">Save as...</property> |
320 | + <property name="use_underline">True</property> |
321 | + <property name="stock_id">gtk-save-as</property> |
322 | + <signal name="clicked" handler="save_as_menuitem_activate_cb"/> |
323 | + </object> |
324 | + <packing> |
325 | + <property name="expand">False</property> |
326 | + <property name="homogeneous">True</property> |
327 | + </packing> |
328 | + </child> |
329 | + <child> |
330 | <object class="GtkToolButton" id="revert_toolbutton"> |
331 | <property name="visible">True</property> |
332 | <property name="sensitive">False</property> |
333 | @@ -916,7 +941,11 @@ |
334 | <property name="visible">True</property> |
335 | <property name="stock">gtk-save</property> |
336 | </object> |
337 | - <object class="GtkImage" id="save_as_image"> |
338 | + <object class="GtkImage" id="save_as_image_1"> |
339 | + <property name="visible">True</property> |
340 | + <property name="stock">gtk-save-as</property> |
341 | + </object> |
342 | + <object class="GtkImage" id="save_as_image_2"> |
343 | <property name="visible">True</property> |
344 | <property name="stock">gtk-save-as</property> |
345 | </object> |
346 | |
347 | === modified file 'checkbox_editor/model.py' |
348 | --- checkbox_editor/model.py 2010-08-31 10:10:41 +0000 |
349 | +++ checkbox_editor/model.py 2010-09-02 16:41:06 +0000 |
350 | @@ -10,7 +10,7 @@ |
351 | |
352 | from .data import DataParser, DataEncoder, InternalParsingError, Reporter |
353 | from .vc import VersionControl |
354 | -from .util import handle_exceptions, ExceptionDialog, ExceptionHandled |
355 | +from .util import handle_exceptions, ExceptionDialog, ExceptionHandled, MessageDialogRunner |
356 | |
357 | |
358 | class RenameOverwriteError(Exception): |
359 | @@ -58,13 +58,6 @@ |
360 | |
361 | self.clear() |
362 | |
363 | - # Widgets that must be enabled/disabled |
364 | - # when some change happens to the model |
365 | - on_change_widget_names = ['save_menuitem', 'save_toolbutton', |
366 | - 'revert_menuitem', 'revert_toolbutton'] |
367 | - self.on_change_widgets = [builder.get_object(widget_name) |
368 | - for widget_name in on_change_widget_names] |
369 | - |
370 | |
371 | def clear(self): |
372 | """ |
373 | @@ -80,6 +73,13 @@ |
374 | self.files_to_rename = {} |
375 | gtk.TreeStore.clear(self) |
376 | |
377 | + # Disable all save/revert options |
378 | + for widget_name in ['save_menuitem', 'save_toolbutton', |
379 | + 'save_as_menuitem', 'save_as_toolbutton', |
380 | + 'revert_menuitem', 'revert_toolbutton']: |
381 | + widget = self.builder.get_object(widget_name) |
382 | + widget.set_sensitive(False) |
383 | + |
384 | |
385 | def get_all_rows(self, parent = None): |
386 | """ |
387 | @@ -126,55 +126,84 @@ |
388 | Discard changes and reload data from directory |
389 | """ |
390 | logging.info('Reverting changes') |
391 | - self.load() |
392 | - |
393 | - # Disable save/revert options |
394 | - for widget in self.on_change_widgets: |
395 | - widget.set_sensitive(False) |
396 | - |
397 | - self.files_to_remove = [] |
398 | - self.files_to_rename = {} |
399 | + self.load(self.directory) |
400 | |
401 | |
402 | def is_valid_directory(self, directory): |
403 | + """ |
404 | + Return True if it's a valid directory and data should be loaded |
405 | + In case of not enough permissions, the user might be prompted |
406 | + to use read-only mode |
407 | + """ |
408 | + # Make sure directory exists |
409 | + if not os.path.isdir(directory): |
410 | + message = ("Selected directory doesn't seem to be a valid " |
411 | + 'shared checkbox directory:\n{0}\n\n' |
412 | + 'Please make sure that the path <b>exists</b> ' |
413 | + "and that it's a directory" |
414 | + .format(directory)) |
415 | + MessageDialogRunner('Invalid directory', message).run() |
416 | + return False |
417 | + |
418 | + # Make sure that it's possible to access to selected directory |
419 | + if not os.access(directory, os.F_OK | os.R_OK | os.X_OK): |
420 | + message = ('Not enough <b>permissions</b> to access selected directory:\n{0}\n\n' |
421 | + 'Please change permissions accordingly or run checkbox-editor ' |
422 | + 'with from a different user account with sufficient permissions.' |
423 | + .format(directory)) |
424 | + MessageDialogRunner('Insuficcient permissions', message).run() |
425 | + return False |
426 | + |
427 | + # Make sure that directory contains a jobs subdirectory |
428 | jobs_dir = os.path.join(directory, 'jobs') |
429 | - if not (os.path.isdir(directory) |
430 | - and os.path.isdir(jobs_dir)): |
431 | - dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, |
432 | - buttons=gtk.BUTTONS_OK) |
433 | - dialog.set_title('Invalid directory') |
434 | + if not os.path.isdir(jobs_dir): |
435 | message = ("Selected directory doesn't seem to be a valid " |
436 | - 'shared checkbox directory. Please make sure ' |
437 | - 'to select a directory that at least contains ' |
438 | - 'a <b>jobs</b> subdirectory.') |
439 | - dialog.set_markup(message) |
440 | - dialog.run() |
441 | - dialog.destroy() |
442 | + 'shared checkbox directory:\n{0}\n\n' |
443 | + 'Please make sure to select a directory ' |
444 | + 'that at least contains a <b>jobs</b> subdirectory.' |
445 | + .format(directory)) |
446 | + MessageDialogRunner('Invalid directory', message).run() |
447 | return False |
448 | |
449 | + # Make sure that it's possible to create/delete files |
450 | + # in the selected directory |
451 | + if not os.access(directory, os.F_OK | os.W_OK): |
452 | + message = ('Not enough <b>permissions</b> to write files ' |
453 | + 'to selected directory:\n{0}\n\n' |
454 | + 'Unless permissions are changed accordingly ' |
455 | + 'or checkbox-editor is executed ' |
456 | + 'from a different user account with sufficient permissions, ' |
457 | + "it won't be possible to save changes to the same directory.\n\n" |
458 | + 'Would you like to continue?' |
459 | + .format(directory)) |
460 | + response = MessageDialogRunner('Insuficcient permissions', |
461 | + message, |
462 | + type=gtk.MESSAGE_WARNING, |
463 | + buttons=gtk.BUTTONS_YES_NO).run() |
464 | + if response != gtk.RESPONSE_YES: |
465 | + return False |
466 | + |
467 | return True |
468 | |
469 | |
470 | @handle_exceptions(InternalParsingError, |
471 | 'Load operation <b>failed</b>') |
472 | - def load(self, directory = None): |
473 | + def load(self, directory): |
474 | """ |
475 | Load data from directory |
476 | """ |
477 | self.clear() |
478 | - self.testplan = None |
479 | - |
480 | - if directory: |
481 | - self.directory = directory |
482 | - self.jobs_dir = os.path.join(directory, 'jobs') |
483 | - self.scripts_dir = os.path.join(directory, 'scripts') |
484 | - self.vc = VersionControl.get(directory, |
485 | - self.preferences.version_control['exclude']) |
486 | - |
487 | - if self.vc.has_changes(): |
488 | - for widget_name in ['commit_menuitem', 'commit_toolbutton']: |
489 | - widget = self.builder.get_object(widget_name) |
490 | - widget.set_sensitive(True) |
491 | + |
492 | + self.directory = directory |
493 | + self.jobs_dir = os.path.join(directory, 'jobs') |
494 | + self.scripts_dir = os.path.join(directory, 'scripts') |
495 | + self.vc = VersionControl.get(directory, |
496 | + self.preferences.version_control['exclude']) |
497 | + |
498 | + if self.vc.has_changes(): |
499 | + for widget_name in ['commit_menuitem', 'commit_toolbutton']: |
500 | + widget = self.builder.get_object(widget_name) |
501 | + widget.set_sensitive(True) |
502 | |
503 | if hasattr(self.vc, 'pull'): |
504 | for widget_name in ['pull_menuitem', 'pull_toolbutton']: |
505 | @@ -187,12 +216,12 @@ |
506 | if not pulled: |
507 | return ExceptionHandled |
508 | |
509 | + self._set_on_change_widgets() |
510 | logging.info('Loading jobs: {0}'.format(self.directory)) |
511 | |
512 | # Little trick that makes parsing work |
513 | # when relative directories are in preferences |
514 | - if directory: |
515 | - os.chdir(directory) |
516 | + os.chdir(directory) |
517 | |
518 | job_filename_pattern = re.compile('\.txt(\.in)?$') |
519 | jobs_filenames = [os.path.join(self.jobs_dir, file) |
520 | @@ -286,6 +315,19 @@ |
521 | load_job_file(file_row, job_filename) |
522 | |
523 | |
524 | + def _set_on_change_widgets(self): |
525 | + """ |
526 | + Set the names of the widgets that are subject to be activated |
527 | + on any change in the model based on the opened directory |
528 | + """ |
529 | + on_change_widget_names = ['save_as_menuitem', 'save_as_toolbutton', |
530 | + 'revert_menuitem', 'revert_toolbutton'] |
531 | + if os.access(self.directory, os.F_OK | os.W_OK): |
532 | + on_change_widget_names.extend(['save_menuitem', 'save_toolbutton']) |
533 | + self.on_change_widgets = [self.builder.get_object(widget_name) |
534 | + for widget_name in on_change_widget_names] |
535 | + |
536 | + |
537 | @handle_exceptions((EnvironmentError, RenameOverwriteError), |
538 | 'Save operation <b>failed</b>') |
539 | def save(self): |
540 | @@ -502,6 +544,43 @@ |
541 | row[self.FONT_DESCRIPTION] = pango.FontDescription() |
542 | |
543 | |
544 | + def save_as(self, destination_directory): |
545 | + """ |
546 | + Save changes to a different directory |
547 | + """ |
548 | + source_directory = self.directory |
549 | + self.vc.clone(destination_directory) |
550 | + |
551 | + # Update all directory dependent variables in model |
552 | + logging.debug('Updating internal state to use new directory: {0!r}' |
553 | + .format(destination_directory)) |
554 | + self.directory = destination_directory |
555 | + self.jobs_dir = os.path.join(destination_directory, 'jobs') |
556 | + self.scripts_dir = os.path.join(destination_directory, 'scripts') |
557 | + self.vc = VersionControl.get(destination_directory, |
558 | + self.preferences.version_control['exclude']) |
559 | + def change_base_directory(filename): |
560 | + return os.path.join(destination_directory, |
561 | + os.path.relpath(filename, source_directory)) |
562 | + |
563 | + self.files_to_remove = [change_base_directory(filename) |
564 | + for filename in self.files_to_remove] |
565 | + self.files_to_rename = dict([(change_base_directory(old_filename), |
566 | + change_base_directory(new_filename)) |
567 | + for old_filename, new_filename |
568 | + in self.files_to_rename.items()]) |
569 | + |
570 | + # Update all rows filenames |
571 | + for row in self.get_all_rows(): |
572 | + filename = row[self.FILENAME] |
573 | + if filename: |
574 | + row[self.FILENAME] = change_base_directory(filename) |
575 | + |
576 | + self.save() |
577 | + self.preferences.directory = destination_directory |
578 | + self._set_on_change_widgets() |
579 | + |
580 | + |
581 | def add_row(self, iterator): |
582 | """ |
583 | Add a new row to the model |
584 | |
585 | === modified file 'checkbox_editor/preferences.py' |
586 | --- checkbox_editor/preferences.py 2010-08-31 10:10:41 +0000 |
587 | +++ checkbox_editor/preferences.py 2010-09-02 16:41:06 +0000 |
588 | @@ -9,7 +9,7 @@ |
589 | import shlex |
590 | from glob import glob |
591 | |
592 | -from .util import CommandConfigParser as ConfigParser |
593 | +from .util import MessageDialogRunner, CommandConfigParser as ConfigParser |
594 | |
595 | VC_WIDGET_NAMES = ('pull_on_open', 'commit_on_save', 'push_on_close', |
596 | 'pull_branch', 'push_branch') |
597 | @@ -339,12 +339,7 @@ |
598 | version_control) |
599 | except IOError as exception: |
600 | logging.error(exception) |
601 | - dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, |
602 | - buttons=gtk.BUTTONS_OK, |
603 | - message_format=str(exception)) |
604 | - dialog.set_title('Error') |
605 | - dialog.run() |
606 | - dialog.destroy() |
607 | + MessageDialogRunner('Error', str(exception)).run() |
608 | else: |
609 | self.dialog.destroy() |
610 | |
611 | @@ -470,42 +465,33 @@ |
612 | filename = None |
613 | response = dialog.run() |
614 | if response == gtk.RESPONSE_ACCEPT: |
615 | + abs_filename = dialog.get_filename() |
616 | filename = os.path.relpath(dialog.get_filename(), |
617 | self.preferences.directory) |
618 | if filename.startswith(os.path.pardir): |
619 | - log_message = ('Selected filename must be ' |
620 | - 'under opened directory: {0}' |
621 | - .format(self.preferences.directory)) |
622 | + log_message = ('Selected filename {0!r} must be ' |
623 | + 'under opened directory {1!r}' |
624 | + .format(abs_filename, |
625 | + self.preferences.directory)) |
626 | logging.error(log_message) |
627 | |
628 | |
629 | - dialog_message = ('Selected filename must be ' |
630 | - 'under opened directory:\n<b>{0}</b>' |
631 | - .format(self.preferences.directory)) |
632 | - error_dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL, |
633 | - type=gtk.MESSAGE_ERROR, |
634 | - buttons=gtk.BUTTONS_OK) |
635 | - error_dialog.set_title('Error') |
636 | - error_dialog.set_markup(dialog_message) |
637 | - error_dialog.run() |
638 | - error_dialog.destroy() |
639 | + dialog_message = ('Selected filename:\n{0}\n' |
640 | + 'must be under opened directory:\n{1}' |
641 | + .format(abs_filename, |
642 | + self.preferences.directory)) |
643 | + MessageDialogRunner('Error', dialog_message).run() |
644 | continue |
645 | |
646 | if filename in filenames and filename != initial_filename: |
647 | - log_message = ('Filename is already selected: {0}' |
648 | - .format(filename)) |
649 | + log_message = ('Filename is already selected: {0!r}' |
650 | + .format(abs_filename)) |
651 | logging.error(log_message) |
652 | |
653 | |
654 | - dialog_message = ('Filename is already selected:\n<b>{0}</b>' |
655 | - .format(filename)) |
656 | - error_dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL, |
657 | - type=gtk.MESSAGE_ERROR, |
658 | - buttons=gtk.BUTTONS_OK) |
659 | - error_dialog.set_title('Error') |
660 | - error_dialog.set_markup(dialog_message) |
661 | - error_dialog.run() |
662 | - error_dialog.destroy() |
663 | + MessageDialogRunner('Error', |
664 | + 'Filename is already selected:\n{0}' |
665 | + .format(abs_filename)).run() |
666 | continue |
667 | break |
668 | |
669 | |
670 | === modified file 'checkbox_editor/treeview.py' |
671 | --- checkbox_editor/treeview.py 2010-06-03 08:49:41 +0000 |
672 | +++ checkbox_editor/treeview.py 2010-09-02 16:41:06 +0000 |
673 | @@ -5,6 +5,7 @@ |
674 | pygtk.require("2.0") |
675 | import gtk |
676 | from .model import TreeStore |
677 | +from .util import MessageDialogRunner |
678 | |
679 | import os, re, logging |
680 | |
681 | @@ -99,16 +100,16 @@ |
682 | self.builder.get_object('job_command_edit').set_sensitive(False) |
683 | |
684 | logging.debug('Selection changed:\n' |
685 | - '- display_name: {0}\n' |
686 | - '- description: {1}\n' |
687 | - '- filename: {2}\n' |
688 | + '- display_name: {0!r}\n' |
689 | + '- description: {1!r}\n' |
690 | + '- filename: {2!r}\n' |
691 | '- modified: {3}\n' |
692 | '- children_modified: {4}\n' |
693 | '- children_removed: {5}\n' |
694 | '- selected: {6}' |
695 | - .format(repr(row[TreeStore.DISPLAY_NAME]), |
696 | - repr(row[TreeStore.DESCRIPTION]), |
697 | - repr(row[TreeStore.FILENAME]), |
698 | + .format(row[TreeStore.DISPLAY_NAME], |
699 | + row[TreeStore.DESCRIPTION], |
700 | + row[TreeStore.FILENAME], |
701 | row[TreeStore.MODIFIED], |
702 | row[TreeStore.CHILDREN_MODIFIED], |
703 | row[TreeStore.CHILDREN_REMOVED], |
704 | @@ -285,30 +286,20 @@ |
705 | |
706 | # Validate filename |
707 | if depth == 0 and not re.search('\.txt(\.in)?$', new_name): |
708 | - dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, |
709 | - buttons=gtk.BUTTONS_OK) |
710 | - dialog.set_title('Invalid filename') |
711 | - message = ('Invalid filename: <b>{0}</b>\n' |
712 | - 'The extension should be either <b>.txt.in</b> or <b>.txt</b>' |
713 | - .format(new_name)) |
714 | - dialog.set_markup(message) |
715 | - dialog.run() |
716 | - dialog.destroy() |
717 | - return |
718 | + MessageDialogRunner('Invalid filename', |
719 | + 'Invalid filename: <b>{0}</b>\n' |
720 | + 'The extension should be either <b>.txt.in</b> or <b>.txt</b>' |
721 | + .format(new_name)).run() |
722 | + return |
723 | |
724 | # Validate name |
725 | new_name_exists = any(r for r in model.get_all_rows() |
726 | if (r[TreeStore.DISPLAY_NAME] == new_name |
727 | and r.path != row.path)) |
728 | if new_name_exists: |
729 | - dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, |
730 | - buttons=gtk.BUTTONS_OK) |
731 | - dialog.set_title('Invalid name') |
732 | - message = ('<b>{0}</b> is already being used in another row' |
733 | - .format(new_name)) |
734 | - dialog.set_markup(message) |
735 | - dialog.run() |
736 | - dialog.destroy() |
737 | + MessageDialogRunner('Invalid name', |
738 | + '<b>{0}</b> is already being used in another row' |
739 | + .format(new_name)).run() |
740 | return |
741 | |
742 | # Set display name |
743 | @@ -354,27 +345,21 @@ |
744 | and (len(destination_row.path) > 1 |
745 | or position in (gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, |
746 | gtk.TREE_VIEW_DROP_INTO_OR_AFTER))): |
747 | - error_message = ('Root nodes can only be moved\n' |
748 | + error_message = ('Root nodes can <b>only</b> be moved\n' |
749 | 'to other position at the same level') |
750 | elif (len(origin_row.path) > 1 and len(destination_row.path) <= 1 |
751 | and position in (gtk.TREE_VIEW_DROP_BEFORE, |
752 | gtk.TREE_VIEW_DROP_AFTER)): |
753 | - error_message = ("Job nodes can't be moved\n" |
754 | + error_message = ("Job nodes <b>can't</b> be moved\n" |
755 | 'to the root of the tree') |
756 | elif (origin_row.path == destination_row.path[:len(origin_row.path)]): |
757 | - error_message = ("Job nodes can't be moved\n" |
758 | + error_message = ("Job nodes <b>can't</b> be moved\n" |
759 | 'to a position under themselves') |
760 | |
761 | # If error_message is set, then display message |
762 | # and cancel drag&drop operation |
763 | if error_message: |
764 | - dialog = gtk.MessageDialog(flags = gtk.DIALOG_MODAL, |
765 | - type = gtk.MESSAGE_ERROR, |
766 | - buttons = gtk.BUTTONS_OK, |
767 | - message_format = error_message) |
768 | - dialog.set_title('Error') |
769 | - dialog.run() |
770 | - dialog.destroy() |
771 | + MessageDialogRunner('Error', error_message).run() |
772 | context.finish(False, False, timestamp) |
773 | return |
774 | |
775 | |
776 | === modified file 'checkbox_editor/util.py' |
777 | --- checkbox_editor/util.py 2010-06-03 08:49:41 +0000 |
778 | +++ checkbox_editor/util.py 2010-09-02 16:41:06 +0000 |
779 | @@ -262,3 +262,35 @@ |
780 | stdout, _ = process.communicate() |
781 | stdout = stdout.splitlines()[0] |
782 | return stdout |
783 | + |
784 | + |
785 | +class MessageDialogRunner(object): |
786 | + """ |
787 | + Create a MessageDialog, run it, destroy it and return response |
788 | + """ |
789 | + def __init__(self, title, message, |
790 | + type=gtk.MESSAGE_ERROR, |
791 | + buttons=gtk.BUTTONS_OK, |
792 | + flags=gtk.DIALOG_MODAL): |
793 | + """ |
794 | + Create MessageDialog object |
795 | + """ |
796 | + self.title = title |
797 | + self.message = message |
798 | + self.type = type |
799 | + self.buttons = buttons |
800 | + self.flags = flags |
801 | + |
802 | + |
803 | + def run(self): |
804 | + """ |
805 | + Run dialog, destroy it and return response |
806 | + """ |
807 | + dialog = gtk.MessageDialog(type=self.type, |
808 | + buttons=self.buttons, |
809 | + flags=self.flags) |
810 | + dialog.set_title(self.title) |
811 | + dialog.set_markup(self.message) |
812 | + response = dialog.run() |
813 | + dialog.destroy() |
814 | + return response |
815 | |
816 | === modified file 'checkbox_editor/vc.py' |
817 | --- checkbox_editor/vc.py 2010-06-03 08:49:41 +0000 |
818 | +++ checkbox_editor/vc.py 2010-09-02 16:41:06 +0000 |
819 | @@ -5,7 +5,7 @@ |
820 | pygtk.require("2.0") |
821 | import gtk |
822 | |
823 | -import os, logging, threading |
824 | +import os, logging, threading, shutil |
825 | from cStringIO import StringIO |
826 | |
827 | from contextlib import contextmanager |
828 | @@ -17,7 +17,7 @@ |
829 | import bzrlib.plugin |
830 | bzrlib.plugin.load_plugins() |
831 | |
832 | -from .util import handle_exceptions |
833 | +from .util import handle_exceptions, MessageDialogRunner |
834 | |
835 | |
836 | class VersionControl: |
837 | @@ -209,14 +209,9 @@ |
838 | Push changes |
839 | """ |
840 | if not branch_url: |
841 | - dialog = gtk.MessageDialog(type = gtk.MESSAGE_ERROR, |
842 | - flags = gtk.DIALOG_MODAL, |
843 | - buttons = gtk.BUTTONS_OK) |
844 | - dialog.set_title('Error') |
845 | - dialog.set_markup('No push branch defined in preferences.') |
846 | - dialog.show_all() |
847 | - dialog.run() |
848 | - dialog.destroy() |
849 | + MessageDialogRunner('Error', |
850 | + 'Push branch <b>not</b> defined ' |
851 | + 'in preferences.').run() |
852 | return False |
853 | |
854 | logging.debug('Pushing changes to: {0}'.format(repr(branch_url))) |
855 | @@ -243,14 +238,9 @@ |
856 | Pull changes |
857 | """ |
858 | if not branch_url: |
859 | - dialog = gtk.MessageDialog(type = gtk.MESSAGE_ERROR, |
860 | - flags = gtk.DIALOG_MODAL, |
861 | - buttons = gtk.BUTTONS_OK) |
862 | - dialog.set_title('Error') |
863 | - dialog.set_markup('No pull branch defined in preferences.') |
864 | - dialog.show_all() |
865 | - dialog.run() |
866 | - dialog.destroy() |
867 | + MessageDialogRunner('Error', |
868 | + 'Pull branch <b>not</b> defined ' |
869 | + 'in preferences.').run() |
870 | return False |
871 | |
872 | logging.debug('Pulling changes from: {0}'.format(repr(branch_url))) |
873 | @@ -356,6 +346,15 @@ |
874 | dialog.destroy() |
875 | |
876 | |
877 | + def clone(self, destination_directory): |
878 | + """ |
879 | + Clone working directory to destination |
880 | + """ |
881 | + logging.debug('Cloning bzr repository from {0!r} to {1!r}' |
882 | + .format(self.directory, destination_directory)) |
883 | + self.working_tree.bzrdir.sprout(destination_directory) |
884 | + |
885 | + |
886 | class NullVC(VersionControl): |
887 | """ |
888 | No version control at all, just files |
889 | @@ -393,3 +392,23 @@ |
890 | Rename file in the file system |
891 | """ |
892 | os.rename(old_name, new_name) |
893 | + |
894 | + |
895 | + def clone(self, destination_directory): |
896 | + """ |
897 | + Copy all files to a remote directory |
898 | + """ |
899 | + # Directory must not exist for copytree to work |
900 | + # but FileChooserDialog creates it, so here it's |
901 | + # removed before trying to copy the contents |
902 | + # of the original directory |
903 | + os.rmdir(destination_directory) |
904 | + |
905 | + # Copy all files in the old shared directory to the new one |
906 | + source_directory = self.directory |
907 | + logging.debug('Copying files from {0!r} to {1!r}' |
908 | + .format(source_directory, destination_directory)) |
909 | + shutil.copytree(source_directory, destination_directory, symlinks=True) |
910 | + |
911 | + |
912 | +5175 |
913 | |
914 | === modified file 'debian/changelog' |
915 | --- debian/changelog 2010-08-31 10:22:18 +0000 |
916 | +++ debian/changelog 2010-09-02 16:41:06 +0000 |
917 | @@ -1,3 +1,10 @@ |
918 | +checkbox-editor (0.9-0ubuntu1~ppa43) lucid; urgency=low |
919 | + |
920 | + * Message dialog warns user on shared directory insufficient permissions (LP: #619720) |
921 | + * 'Save as' functionality implemented (LP: #619720) |
922 | + |
923 | + -- Javier Collado <javier.collado@canonical.com> Thu, 02 Sep 2010 18:29:13 +0200 |
924 | + |
925 | checkbox-editor (0.9-0ubuntu1~ppa42) lucid; urgency=low |
926 | |
927 | * Added tooltips to toolbuttons (LP: #619710) |