← Back to team overview

oem-community-qa team mailing list archive

[Merge] lp:~oem-community-qa/checkbox-editor/bug619720 into lp:checkbox-editor

 

Javier Collado has proposed merging lp:~oem-community-qa/checkbox-editor/bug619720 into lp:checkbox-editor.

Requested reviews:
  Javier Collado (javier.collado)
Related bugs:
  #619720 Can not run a new test case created with checkbox-editor (nor can you save it) if you don't run as root
  https://bugs.launchpad.net/bugs/619720


Message dialog warns user on shared directory insufficient permissions
'Save as' functionality implemented

-- 
https://code.launchpad.net/~oem-community-qa/checkbox-editor/bug619720/+merge/34447
Your team OEM Community QA is subscribed to branch lp:~oem-community-qa/checkbox-editor/bug619720.
=== modified file 'checkbox_editor/editor.py'
--- checkbox_editor/editor.py	2010-08-31 10:10:41 +0000
+++ checkbox_editor/editor.py	2010-09-02 16:41:06 +0000
@@ -17,7 +17,7 @@
 from .treeview import Treeview
 from .form import Form
 from .statusbar import StatusBar
-from .util import CommandLauncher, ExceptionHandled, exception_handler
+from .util import CommandLauncher, ExceptionHandled, exception_handler, MessageDialogRunner
 from . import build
 
 
@@ -63,6 +63,7 @@
                 'close_menuitem_activate_cb': self.close_menuitem_activate_cb,
                 'revert_menuitem_activate_cb': self.revert_menuitem_activate_cb,
                 'save_menuitem_activate_cb': self.save_menuitem_activate_cb,
+                'save_as_menuitem_activate_cb': self.save_as_menuitem_activate_cb,
                 'open_testplan_menuitem_activate_cb': self.open_testplan_menuitem_activate_cb,
                 'save_testplan_menuitem_activate_cb': self.save_testplan_menuitem_activate_cb,
                 'save_testplan_as_menuitem_activate_cb': self.save_testplan_as_menuitem_activate_cb,
@@ -109,6 +110,7 @@
                 for widget_name in widget_names:
                     self.builder.get_object(widget_name).set_sensitive(True)
 
+                self.set_title(options.directory)
                 self.statusbar._write_tmp_message('Shared directory opened')
 
         treeview = self.builder.get_object('treeview')
@@ -173,19 +175,20 @@
         window.add_accel_group(group)
 
         accelerators_data = [
-            ('open_menuitem', 'o'),
-            ('close_menuitem', 'w'),
-            ('quit_menuitem', 'q'),
-            ('save_menuitem', 's'),
-            ('add_menuitem', 'a'),
-            ('remove_menuitem', 'r'),
-            ('preferences_menuitem', 'p'),
-            ('contents_menuitem', 'F1'),
+            ('open_menuitem', '<Control>o'),
+            ('close_menuitem', '<Control>w'),
+            ('quit_menuitem', '<Control>q'),
+            ('save_menuitem', '<Control>s'),
+            ('save_as_menuitem', '<Shift><Control>s'),
+            ('add_menuitem', '<Control>a'),
+            ('remove_menuitem', '<Control>r'),
+            ('preferences_menuitem', '<Control>p'),
+            ('contents_menuitem', '<Control>F1'),
             ]
 
-        for widget_name, key in accelerators_data:
+        for widget_name, keys in accelerators_data:
             menuitem = self.builder.get_object(widget_name)
-            accel_key, accel_mod = gtk.accelerator_parse("<Control>%s" % key)
+            accel_key, accel_mod = gtk.accelerator_parse(keys)
             menuitem.add_accelerator("activate",
                                      group,
                                      accel_key,
@@ -238,6 +241,19 @@
         return options
 
 
+    def set_title(self, directory=None):
+        """
+        Set main window title based on the opened directory
+        """
+        title = 'Checkbox Editor'
+        if directory:
+            title = '{0} - '.format(directory) + title
+            if not os.access(directory, os.F_OK | os.W_OK):
+                title = '[read only] ' + title
+        window = self.builder.get_object('window')
+        window.set_title(title)
+
+
     def window_delete_event_cb(self, window, event):
         """
         Delete main window
@@ -294,23 +310,26 @@
         directory = dialog.get_filename()
         dialog.destroy()
 
-        if response == gtk.RESPONSE_ACCEPT:
-            if model.is_valid_directory(directory):
-                self.preferences.load(directory)
-                if ExceptionHandled == model.load(directory):
-                    model.clear()
-                else:
-                    for widget_name in ('close_menuitem', 'close_toolbutton',
-                                        'open_testplan_menuitem'):
-                        widget = self.builder.get_object(widget_name)
-                        widget.set_sensitive(True)
-
-                    for widget_name in ('push_menuitem', 'push_toolbutton',
-                                        'commit_menuitem', 'commit_toolbutton'):
-                        widget = self.builder.get_object(widget_name)
-                        widget.set_sensitive(False)
-
-                    self.statusbar._write_tmp_message('Shared directory opened')
+        if response != gtk.RESPONSE_ACCEPT:
+            return False
+
+        if model.is_valid_directory(directory):
+            self.preferences.load(directory)
+            if ExceptionHandled == model.load(directory):
+                model.clear()
+            else:
+                for widget_name in ('close_menuitem', 'close_toolbutton',
+                                    'open_testplan_menuitem'):
+                    widget = self.builder.get_object(widget_name)
+                    widget.set_sensitive(True)
+
+                for widget_name in ('push_menuitem', 'push_toolbutton',
+                                    'commit_menuitem', 'commit_toolbutton'):
+                    widget = self.builder.get_object(widget_name)
+                    widget.set_sensitive(False)
+
+                self.set_title(directory)
+                self.statusbar._write_tmp_message('Shared directory opened')
 
         return False
 
@@ -322,7 +341,6 @@
         if not self._continue_without_saving():
             return False
 
-
         treeview = self.builder.get_object('treeview')
         model = treeview.get_model()
         model.clear()
@@ -332,13 +350,12 @@
         self.preferences.load()
 
         widget_names = ['close_menuitem', 'close_toolbutton',
-                        'save_menuitem', 'save_toolbutton',
-                        'revert_menuitem', 'revert_toolbutton',
                         'open_testplan_menuitem', 'save_testplan_menuitem',
                         'save_testplan_as_menuitem', 'export_testplan_menuitem']
         for widget_name in widget_names:
             self.builder.get_object(widget_name).set_sensitive(False)
 
+        self.set_title()
         self.statusbar._write_tmp_message('Shared directory closed')
 
         return False
@@ -354,6 +371,54 @@
         if saved is True:
             self.statusbar._write_tmp_message('Changes saved')
 
+        return False
+
+
+    def save_as_menuitem_activate_cb(self, widget):
+        """
+        Save changes to a different directory
+        """
+        # Choose a destination directory
+        dialog = gtk.FileChooserDialog('Choose Destination Shared Directory',
+                                       buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
+                                                  gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT),
+                                       action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
+
+        while True:
+            response = dialog.run()
+            directory = dialog.get_filename()
+
+            if response != gtk.RESPONSE_ACCEPT:
+                dialog.destroy()
+                return False
+
+            if not os.path.isdir(directory):
+                message = ("Selected directory doesn't seem to be a valid "
+                           'directory:\n{0}\n\n'
+                           'Please make sure that the path <b>exists</b> '
+                           "and that it's a directory"
+                           .format(directory))
+                MessageDialogRunner('Invalid directory', message).run()
+
+            elif not os.access(directory, os.F_OK | os.R_OK | os.W_OK | os.X_OK):
+                message = ('Not enough <b>permissions</b> '
+                           'to read/write/execute selected directory:\n{0}\n\n'
+                           .format(directory))
+                MessageDialogRunner('Insuficcient permissions', message).run()
+
+            elif bool(os.listdir(directory)):
+                message = ("Selected directory isn't <b>empty</b>:\n{0}\n\n"
+                           'Please select an empty directory, '
+                           'so that it can be populated with new data properly'
+                           .format(directory))
+                MessageDialogRunner('Not empty directory', message).run()
+            else:
+                break
+        dialog.destroy()
+
+        model = self.builder.get_object('treeview').get_model()
+        model.save_as(directory)
+        self.set_title(directory)
 
         return False
 
@@ -363,18 +428,16 @@
         Check if there are pending changes and ask user
         if he wants to continue
         """
-        # If save menuitem is sensitive,
+        # If save_as_menuitem is sensitive,
         # it means there are pending (not saved) changes
-        if self.builder.get_object('save_menuitem').get_property('sensitive'):
-            message = ('Changes have not been saved.\n\n'
+        if self.builder.get_object('save_as_menuitem').get_property('sensitive'):
+            message = ('Changes have <b>not</b> been saved.\n\n'
                        'Would you like to continue?')
-            dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING,
-                                       buttons=gtk.BUTTONS_YES_NO,
-                                       message_format=message)
-            response = dialog.run()
-            dialog.destroy()
+            response = MessageDialogRunner('Changes not saved', message,
+                                           type=gtk.MESSAGE_WARNING,
+                                           buttons=gtk.BUTTONS_YES_NO).run()
 
-            if response == gtk.RESPONSE_NO:
+            if response != gtk.RESPONSE_YES:
                 return False
 
         return True
@@ -557,13 +620,10 @@
                        'Please note that they are not going to be available\n'
                        'in checkbox.\n\n'
                        'Would you like to continue?')
-            dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL,
-                                       type=gtk.MESSAGE_WARNING,
-                                       buttons=gtk.BUTTONS_YES_NO)
-            dialog.set_markup(message)
-            response = dialog.run()
-            dialog.destroy()
-            if response == gtk.RESPONSE_NO:
+            response = MessageDialogRunner('Changes not saved', message,
+                                           type=gtk.MESSAGE_WARNING,
+                                           buttons=gtk.BUTTONS_YES_NO).run()
+            if response != gtk.RESPONSE_YES:
                 return
 
         treeview = self.builder.get_object('treeview')

=== modified file 'checkbox_editor/form.py'
--- checkbox_editor/form.py	2010-08-31 09:13:14 +0000
+++ checkbox_editor/form.py	2010-09-02 16:41:06 +0000
@@ -8,7 +8,7 @@
 import os, re, logging, shlex
 
 from .model import TreeStore
-from .util import CommandLauncher, SignalBlocker
+from .util import CommandLauncher, SignalBlocker, MessageDialogRunner
 
 class Form(object):
     """
@@ -374,16 +374,12 @@
         description = row[TreeStore.DESCRIPTION]
 
         # Prompt user to confirm change
-        message=('Jobs that use the {0} plugin cannot contain '
+        message=('Jobs that use the {0!r} plugin cannot contain '
                  'children jobs definitions, so they will <b>removed</b>. '
-                 'Is that what you would like to do?'.format(repr(value)))
-        dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING,
-                                   buttons=gtk.BUTTONS_YES_NO)
-        dialog.set_title('Cannot have children')
-        dialog.set_markup(message)
-        response = dialog.run()
-        dialog.destroy()
-
+                 'Is that what you would like to do?'.format(value))
+        response = MessageDialogRunner('Cannot have children', message,
+                                       type=gtk.MESSAGE_WARNING,
+                                       buttons=gtk.BUTTONS_YES_NO).run()
         change_cancelled = False if response == gtk.RESPONSE_YES else True
 
         if change_cancelled:

=== modified file 'checkbox_editor/glade/editor.glade'
--- checkbox_editor/glade/editor.glade	2010-08-31 09:13:14 +0000
+++ checkbox_editor/glade/editor.glade	2010-09-02 16:41:06 +0000
@@ -76,6 +76,16 @@
                       </object>
                     </child>
                     <child>
+                      <object class="GtkImageMenuItem" id="save_as_menuitem">
+                        <property name="label" translatable="yes">Save as...</property>
+                        <property name="visible">True</property>
+                        <property name="sensitive">False</property>
+                        <property name="image">save_as_image_1</property>
+                        <property name="use_stock">False</property>
+                        <signal name="activate" handler="save_as_menuitem_activate_cb"/>
+                      </object>
+                    </child>
+                    <child>
                       <object class="GtkImageMenuItem" id="revert_menuitem">
                         <property name="label">gtk-revert-to-saved</property>
                         <property name="visible">True</property>
@@ -115,7 +125,7 @@
                         <property name="label" translatable="yes">Save test plan as...</property>
                         <property name="visible">True</property>
                         <property name="sensitive">False</property>
-                        <property name="image">save_as_image</property>
+                        <property name="image">save_as_image_2</property>
                         <property name="use_stock">False</property>
                         <signal name="activate" handler="save_testplan_as_menuitem_activate_cb"/>
                       </object>
@@ -354,6 +364,21 @@
               </packing>
             </child>
             <child>
+              <object class="GtkToolButton" id="save_as_toolbutton">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="tooltip_text" translatable="yes">Save changes to a different directory</property>
+                <property name="label" translatable="yes">Save as...</property>
+                <property name="use_underline">True</property>
+                <property name="stock_id">gtk-save-as</property>
+                <signal name="clicked" handler="save_as_menuitem_activate_cb"/>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="homogeneous">True</property>
+              </packing>
+            </child>
+            <child>
               <object class="GtkToolButton" id="revert_toolbutton">
                 <property name="visible">True</property>
                 <property name="sensitive">False</property>
@@ -916,7 +941,11 @@
     <property name="visible">True</property>
     <property name="stock">gtk-save</property>
   </object>
-  <object class="GtkImage" id="save_as_image">
+  <object class="GtkImage" id="save_as_image_1">
+    <property name="visible">True</property>
+    <property name="stock">gtk-save-as</property>
+  </object>
+  <object class="GtkImage" id="save_as_image_2">
     <property name="visible">True</property>
     <property name="stock">gtk-save-as</property>
   </object>

=== modified file 'checkbox_editor/model.py'
--- checkbox_editor/model.py	2010-08-31 10:10:41 +0000
+++ checkbox_editor/model.py	2010-09-02 16:41:06 +0000
@@ -10,7 +10,7 @@
 
 from .data import DataParser, DataEncoder, InternalParsingError, Reporter
 from .vc import VersionControl
-from .util import handle_exceptions, ExceptionDialog, ExceptionHandled
+from .util import handle_exceptions, ExceptionDialog, ExceptionHandled, MessageDialogRunner
 
 
 class RenameOverwriteError(Exception):
@@ -58,13 +58,6 @@
 
         self.clear()
 
-        # Widgets that must be enabled/disabled
-        # when some change happens to the model
-        on_change_widget_names = ['save_menuitem', 'save_toolbutton',
-                                  'revert_menuitem', 'revert_toolbutton']
-        self.on_change_widgets = [builder.get_object(widget_name)
-                                  for widget_name in on_change_widget_names]
-
 
     def clear(self):
         """
@@ -80,6 +73,13 @@
         self.files_to_rename = {}
         gtk.TreeStore.clear(self)
 
+        # Disable all save/revert options
+        for widget_name in ['save_menuitem', 'save_toolbutton',
+                            'save_as_menuitem', 'save_as_toolbutton',
+                            'revert_menuitem', 'revert_toolbutton']:
+            widget = self.builder.get_object(widget_name)
+            widget.set_sensitive(False)
+
 
     def get_all_rows(self, parent = None):
         """
@@ -126,55 +126,84 @@
         Discard changes and reload data from directory
         """
         logging.info('Reverting changes')
-        self.load()
-
-        # Disable save/revert options
-        for widget in self.on_change_widgets:
-            widget.set_sensitive(False)
-
-        self.files_to_remove = []
-        self.files_to_rename = {}
+        self.load(self.directory)
 
 
     def is_valid_directory(self, directory):
+        """
+        Return True if it's a valid directory and data should be loaded
+        In case of not enough permissions, the user might be prompted
+        to use read-only mode
+        """
+        # Make sure directory exists
+        if not os.path.isdir(directory):
+            message = ("Selected directory doesn't seem to be a valid "
+                       'shared checkbox directory:\n{0}\n\n'
+                       'Please make sure that the path <b>exists</b> '
+                       "and that it's a directory"
+                       .format(directory))
+            MessageDialogRunner('Invalid directory', message).run()
+            return False
+
+        # Make sure that it's possible to access to selected directory
+        if not os.access(directory, os.F_OK | os.R_OK | os.X_OK):
+            message = ('Not enough <b>permissions</b> to access selected directory:\n{0}\n\n'
+                       'Please change permissions accordingly or run checkbox-editor '
+                       'with from a different user account with sufficient permissions.'
+                       .format(directory))
+            MessageDialogRunner('Insuficcient permissions', message).run()
+            return False
+
+        # Make sure that directory contains a jobs subdirectory
         jobs_dir = os.path.join(directory, 'jobs')
-        if not (os.path.isdir(directory)
-                and os.path.isdir(jobs_dir)):
-            dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
-                                       buttons=gtk.BUTTONS_OK)
-            dialog.set_title('Invalid directory')
+        if not os.path.isdir(jobs_dir):
             message = ("Selected directory doesn't seem to be a valid "
-                       'shared checkbox directory. Please make sure '
-                       'to select a directory that at least contains '
-                       'a <b>jobs</b> subdirectory.')
-            dialog.set_markup(message)
-            dialog.run()
-            dialog.destroy()
+                       'shared checkbox directory:\n{0}\n\n'
+                       'Please make sure to select a directory '
+                       'that at least contains a <b>jobs</b> subdirectory.'
+                       .format(directory))
+            MessageDialogRunner('Invalid directory', message).run()
             return False
 
+        # Make sure that it's possible to create/delete files
+        # in the selected directory
+        if not os.access(directory, os.F_OK | os.W_OK):
+            message = ('Not enough <b>permissions</b> to write files '
+                       'to selected directory:\n{0}\n\n'
+                       'Unless permissions are changed accordingly '
+                       'or checkbox-editor is executed '
+                       'from a different user account with sufficient permissions, '
+                       "it won't be possible to save changes to the same directory.\n\n"
+                       'Would you like to continue?'
+                       .format(directory))
+            response = MessageDialogRunner('Insuficcient permissions',
+                                           message,
+                                           type=gtk.MESSAGE_WARNING,
+                                           buttons=gtk.BUTTONS_YES_NO).run()
+            if response != gtk.RESPONSE_YES:
+                return False
+
         return True
 
 
     @handle_exceptions(InternalParsingError,
                        'Load operation <b>failed</b>')
-    def load(self, directory = None):
+    def load(self, directory):
         """
         Load data from directory
         """
         self.clear()
-        self.testplan = None
-
-        if directory:
-            self.directory = directory
-            self.jobs_dir = os.path.join(directory, 'jobs')
-            self.scripts_dir = os.path.join(directory, 'scripts')
-            self.vc = VersionControl.get(directory,
-                                         self.preferences.version_control['exclude'])
-
-            if self.vc.has_changes():
-                for widget_name in ['commit_menuitem', 'commit_toolbutton']:
-                    widget = self.builder.get_object(widget_name)
-                    widget.set_sensitive(True)
+
+        self.directory = directory
+        self.jobs_dir = os.path.join(directory, 'jobs')
+        self.scripts_dir = os.path.join(directory, 'scripts')
+        self.vc = VersionControl.get(directory,
+                                     self.preferences.version_control['exclude'])
+
+        if self.vc.has_changes():
+            for widget_name in ['commit_menuitem', 'commit_toolbutton']:
+                widget = self.builder.get_object(widget_name)
+                widget.set_sensitive(True)
 
         if hasattr(self.vc, 'pull'):
             for widget_name in ['pull_menuitem', 'pull_toolbutton']:
@@ -187,12 +216,12 @@
                     if not pulled:
                         return ExceptionHandled
 
+        self._set_on_change_widgets()
         logging.info('Loading jobs: {0}'.format(self.directory))
 
         # Little trick that makes parsing work
         # when relative directories are in preferences
-        if directory:
-            os.chdir(directory)
+        os.chdir(directory)
 
         job_filename_pattern = re.compile('\.txt(\.in)?$')
         jobs_filenames = [os.path.join(self.jobs_dir, file)
@@ -286,6 +315,19 @@
             load_job_file(file_row, job_filename)
 
 
+    def _set_on_change_widgets(self):
+        """
+        Set the names of the widgets that are subject to be activated
+        on any change in the model based on the opened directory
+        """
+        on_change_widget_names = ['save_as_menuitem', 'save_as_toolbutton',
+                                  'revert_menuitem', 'revert_toolbutton']
+        if os.access(self.directory, os.F_OK | os.W_OK):
+            on_change_widget_names.extend(['save_menuitem', 'save_toolbutton'])
+        self.on_change_widgets = [self.builder.get_object(widget_name)
+                                  for widget_name in on_change_widget_names]
+
+
     @handle_exceptions((EnvironmentError, RenameOverwriteError),
                        'Save operation <b>failed</b>')
     def save(self):
@@ -502,6 +544,43 @@
                 row[self.FONT_DESCRIPTION] = pango.FontDescription()
 
 
+    def save_as(self, destination_directory):
+        """
+        Save changes to a different directory
+        """
+        source_directory = self.directory
+        self.vc.clone(destination_directory)
+
+        # Update all directory dependent variables in model
+        logging.debug('Updating internal state to use new directory: {0!r}'
+                      .format(destination_directory))
+        self.directory = destination_directory
+        self.jobs_dir = os.path.join(destination_directory, 'jobs')
+        self.scripts_dir = os.path.join(destination_directory, 'scripts')
+        self.vc = VersionControl.get(destination_directory,
+                                     self.preferences.version_control['exclude'])
+        def change_base_directory(filename):
+            return os.path.join(destination_directory,
+                                os.path.relpath(filename, source_directory))
+
+        self.files_to_remove = [change_base_directory(filename)
+                                for filename in self.files_to_remove]
+        self.files_to_rename = dict([(change_base_directory(old_filename),
+                                      change_base_directory(new_filename))
+                                     for old_filename, new_filename
+                                     in self.files_to_rename.items()])
+
+        # Update all rows filenames
+        for row in self.get_all_rows():
+            filename = row[self.FILENAME]
+            if filename:
+                row[self.FILENAME] = change_base_directory(filename)
+
+        self.save()
+        self.preferences.directory = destination_directory
+        self._set_on_change_widgets()
+
+
     def add_row(self, iterator):
         """
         Add a new row to the model

=== modified file 'checkbox_editor/preferences.py'
--- checkbox_editor/preferences.py	2010-08-31 10:10:41 +0000
+++ checkbox_editor/preferences.py	2010-09-02 16:41:06 +0000
@@ -9,7 +9,7 @@
 import shlex
 from glob import glob
 
-from .util import CommandConfigParser as ConfigParser
+from .util import MessageDialogRunner, CommandConfigParser as ConfigParser
 
 VC_WIDGET_NAMES = ('pull_on_open', 'commit_on_save', 'push_on_close',
                    'pull_branch', 'push_branch')
@@ -339,12 +339,7 @@
                                                               version_control)
             except IOError as exception:
                 logging.error(exception)
-                dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
-                                           buttons=gtk.BUTTONS_OK,
-                                           message_format=str(exception))
-                dialog.set_title('Error')
-                dialog.run()
-                dialog.destroy()
+                MessageDialogRunner('Error', str(exception)).run()
         else:
             self.dialog.destroy()
 
@@ -470,42 +465,33 @@
             filename = None
             response = dialog.run()
             if response == gtk.RESPONSE_ACCEPT:
+                abs_filename = dialog.get_filename()
                 filename = os.path.relpath(dialog.get_filename(),
                                            self.preferences.directory)
                 if filename.startswith(os.path.pardir):
-                    log_message = ('Selected filename must be '
-                                   'under opened directory: {0}'
-                                   .format(self.preferences.directory))
+                    log_message = ('Selected filename {0!r} must be '
+                                   'under opened directory {1!r}'
+                                   .format(abs_filename,
+                                           self.preferences.directory))
                     logging.error(log_message)
 
 
-                    dialog_message = ('Selected filename must be '
-                                      'under opened directory:\n<b>{0}</b>'
-                                      .format(self.preferences.directory))
-                    error_dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL,
-                                                     type=gtk.MESSAGE_ERROR,
-                                                     buttons=gtk.BUTTONS_OK)
-                    error_dialog.set_title('Error')
-                    error_dialog.set_markup(dialog_message)
-                    error_dialog.run()
-                    error_dialog.destroy()
+                    dialog_message = ('Selected filename:\n{0}\n'
+                                      'must be under opened directory:\n{1}'
+                                      .format(abs_filename,
+                                              self.preferences.directory))
+                    MessageDialogRunner('Error', dialog_message).run()
                     continue
 
                 if filename in filenames and filename != initial_filename:
-                    log_message = ('Filename is already selected: {0}'
-                                   .format(filename))
+                    log_message = ('Filename is already selected: {0!r}'
+                                   .format(abs_filename))
                     logging.error(log_message)
 
 
-                    dialog_message = ('Filename is already selected:\n<b>{0}</b>'
-                                      .format(filename))
-                    error_dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL,
-                                                     type=gtk.MESSAGE_ERROR,
-                                                     buttons=gtk.BUTTONS_OK)
-                    error_dialog.set_title('Error')
-                    error_dialog.set_markup(dialog_message)
-                    error_dialog.run()
-                    error_dialog.destroy()
+                    MessageDialogRunner('Error',
+                                        'Filename is already selected:\n{0}'
+                                        .format(abs_filename)).run()
                     continue
             break
 

=== modified file 'checkbox_editor/treeview.py'
--- checkbox_editor/treeview.py	2010-06-03 08:49:41 +0000
+++ checkbox_editor/treeview.py	2010-09-02 16:41:06 +0000
@@ -5,6 +5,7 @@
 pygtk.require("2.0")
 import gtk
 from .model import TreeStore
+from .util import MessageDialogRunner
 
 import os, re, logging
 
@@ -99,16 +100,16 @@
             self.builder.get_object('job_command_edit').set_sensitive(False)
 
         logging.debug('Selection changed:\n'
-                      '- display_name: {0}\n'
-                      '- description: {1}\n'
-                      '- filename: {2}\n'
+                      '- display_name: {0!r}\n'
+                      '- description: {1!r}\n'
+                      '- filename: {2!r}\n'
                       '- modified: {3}\n'
                       '- children_modified: {4}\n'
                       '- children_removed: {5}\n'
                       '- selected: {6}'
-                      .format(repr(row[TreeStore.DISPLAY_NAME]),
-                              repr(row[TreeStore.DESCRIPTION]),
-                              repr(row[TreeStore.FILENAME]),
+                      .format(row[TreeStore.DISPLAY_NAME],
+                              row[TreeStore.DESCRIPTION],
+                              row[TreeStore.FILENAME],
                               row[TreeStore.MODIFIED],
                               row[TreeStore.CHILDREN_MODIFIED],
                               row[TreeStore.CHILDREN_REMOVED],
@@ -285,30 +286,20 @@
 
         # Validate filename
         if depth == 0 and not re.search('\.txt(\.in)?$', new_name):
-                dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
-                                           buttons=gtk.BUTTONS_OK)
-                dialog.set_title('Invalid filename')
-                message = ('Invalid filename: <b>{0}</b>\n'
-                           'The extension should be either <b>.txt.in</b> or <b>.txt</b>'
-                           .format(new_name))
-                dialog.set_markup(message)
-                dialog.run()
-                dialog.destroy()
-                return
+            MessageDialogRunner('Invalid filename',
+                                'Invalid filename: <b>{0}</b>\n'
+                                'The extension should be either <b>.txt.in</b> or <b>.txt</b>'
+                                .format(new_name)).run()
+            return
 
         # Validate name
         new_name_exists = any(r for r in model.get_all_rows()
                               if (r[TreeStore.DISPLAY_NAME] == new_name
                                   and r.path != row.path))
         if new_name_exists:
-            dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
-                                       buttons=gtk.BUTTONS_OK)
-            dialog.set_title('Invalid name')
-            message = ('<b>{0}</b> is already being used in another row'
-                       .format(new_name))
-            dialog.set_markup(message)
-            dialog.run()
-            dialog.destroy()
+            MessageDialogRunner('Invalid name',
+                                '<b>{0}</b> is already being used in another row'
+                                .format(new_name)).run()
             return
 
         # Set display name
@@ -354,27 +345,21 @@
             and (len(destination_row.path) > 1
                  or position in (gtk.TREE_VIEW_DROP_INTO_OR_BEFORE,
                                  gtk.TREE_VIEW_DROP_INTO_OR_AFTER))):
-            error_message = ('Root nodes can only be moved\n'
+            error_message = ('Root nodes can <b>only</b> be moved\n'
                              'to other position at the same level')
         elif (len(origin_row.path) > 1 and len(destination_row.path) <= 1
             and position in (gtk.TREE_VIEW_DROP_BEFORE,
                              gtk.TREE_VIEW_DROP_AFTER)):
-            error_message = ("Job nodes can't be moved\n"
+            error_message = ("Job nodes <b>can't</b> be moved\n"
                              'to the root of the tree')
         elif (origin_row.path == destination_row.path[:len(origin_row.path)]):
-            error_message = ("Job nodes can't be moved\n"
+            error_message = ("Job nodes <b>can't</b> be moved\n"
                              'to a position under themselves')
 
         # If error_message is set, then display message
         # and cancel drag&drop operation
         if error_message:
-            dialog = gtk.MessageDialog(flags = gtk.DIALOG_MODAL,
-                                       type = gtk.MESSAGE_ERROR,
-                                       buttons = gtk.BUTTONS_OK,
-                                       message_format = error_message)
-            dialog.set_title('Error')
-            dialog.run()
-            dialog.destroy()
+            MessageDialogRunner('Error', error_message).run()
             context.finish(False, False, timestamp)
             return
 

=== modified file 'checkbox_editor/util.py'
--- checkbox_editor/util.py	2010-06-03 08:49:41 +0000
+++ checkbox_editor/util.py	2010-09-02 16:41:06 +0000
@@ -262,3 +262,35 @@
         stdout, _ = process.communicate()
         stdout = stdout.splitlines()[0]
         return stdout
+
+
+class MessageDialogRunner(object):
+    """
+    Create a MessageDialog, run it, destroy it and return response
+    """
+    def __init__(self, title, message,
+                 type=gtk.MESSAGE_ERROR,
+                 buttons=gtk.BUTTONS_OK,
+                 flags=gtk.DIALOG_MODAL):
+        """
+        Create MessageDialog object
+        """
+        self.title = title
+        self.message = message
+        self.type = type
+        self.buttons = buttons
+        self.flags = flags
+
+
+    def run(self):
+        """
+        Run dialog, destroy it and return response
+        """
+        dialog = gtk.MessageDialog(type=self.type,
+                                   buttons=self.buttons,
+                                   flags=self.flags)
+        dialog.set_title(self.title)
+        dialog.set_markup(self.message)
+        response = dialog.run()
+        dialog.destroy()
+        return response

=== modified file 'checkbox_editor/vc.py'
--- checkbox_editor/vc.py	2010-06-03 08:49:41 +0000
+++ checkbox_editor/vc.py	2010-09-02 16:41:06 +0000
@@ -5,7 +5,7 @@
 pygtk.require("2.0")
 import gtk
 
-import os, logging, threading
+import os, logging, threading, shutil
 from cStringIO import StringIO
 
 from contextlib import contextmanager
@@ -17,7 +17,7 @@
 import bzrlib.plugin
 bzrlib.plugin.load_plugins()
 
-from .util import handle_exceptions
+from .util import handle_exceptions, MessageDialogRunner
 
 
 class VersionControl:
@@ -209,14 +209,9 @@
         Push changes
         """
         if not branch_url:
-            dialog = gtk.MessageDialog(type = gtk.MESSAGE_ERROR,
-                                       flags = gtk.DIALOG_MODAL,
-                                       buttons = gtk.BUTTONS_OK)
-            dialog.set_title('Error')
-            dialog.set_markup('No push branch defined in preferences.')
-            dialog.show_all()
-            dialog.run()
-            dialog.destroy()
+            MessageDialogRunner('Error',
+                                'Push branch <b>not</b> defined '
+                                'in preferences.').run()
             return False
 
         logging.debug('Pushing changes to: {0}'.format(repr(branch_url)))
@@ -243,14 +238,9 @@
         Pull changes
         """
         if not branch_url:
-            dialog = gtk.MessageDialog(type = gtk.MESSAGE_ERROR,
-                                       flags = gtk.DIALOG_MODAL,
-                                       buttons = gtk.BUTTONS_OK)
-            dialog.set_title('Error')
-            dialog.set_markup('No pull branch defined in preferences.')
-            dialog.show_all()
-            dialog.run()
-            dialog.destroy()
+            MessageDialogRunner('Error',
+                                'Pull branch <b>not</b> defined '
+                                'in preferences.').run()
             return False
 
         logging.debug('Pulling changes from: {0}'.format(repr(branch_url)))
@@ -356,6 +346,15 @@
         dialog.destroy()
 
 
+    def clone(self, destination_directory):
+        """
+        Clone working directory to destination
+        """
+        logging.debug('Cloning bzr repository from {0!r} to {1!r}'
+                      .format(self.directory, destination_directory))
+        self.working_tree.bzrdir.sprout(destination_directory)
+
+
 class NullVC(VersionControl):
     """
     No version control at all, just files
@@ -393,3 +392,23 @@
         Rename file in the file system
         """
         os.rename(old_name, new_name)
+
+
+    def clone(self, destination_directory):
+        """
+        Copy all files to a remote directory
+        """
+        # Directory must not exist for copytree to work
+        # but FileChooserDialog creates it, so here it's
+        # removed before trying to copy the contents
+        # of the original directory
+        os.rmdir(destination_directory)
+
+        # Copy all files in the old shared directory to the new one
+        source_directory = self.directory
+        logging.debug('Copying files from {0!r} to {1!r}'
+                      .format(source_directory, destination_directory))
+        shutil.copytree(source_directory, destination_directory, symlinks=True)
+
+
+5175

=== modified file 'debian/changelog'
--- debian/changelog	2010-08-31 10:22:18 +0000
+++ debian/changelog	2010-09-02 16:41:06 +0000
@@ -1,3 +1,10 @@
+checkbox-editor (0.9-0ubuntu1~ppa43) lucid; urgency=low
+
+  * Message dialog warns user on shared directory insufficient permissions (LP: #619720)
+  * 'Save as' functionality implemented (LP: #619720)
+
+ -- Javier Collado <javier.collado@xxxxxxxxxxxxx>  Thu, 02 Sep 2010 18:29:13 +0200
+
 checkbox-editor (0.9-0ubuntu1~ppa42) lucid; urgency=low
 
   * Added tooltips to toolbuttons (LP: #619710)


Follow ups