← Back to team overview

gtg team mailing list archive

[Merge] lp:~izidor/gtg/export-rework into lp:gtg

 

Izidor Matušov has proposed merging lp:~izidor/gtg/export-rework into lp:gtg.

Requested reviews:
  Gtg developers (gtg)
Related bugs:
  Bug #896988 in Getting Things GNOME!: "Export and print: Generating PDF does not work"
  https://bugs.launchpad.net/gtg/+bug/896988

For more details, see:
https://code.launchpad.net/~izidor/gtg/export-rework/+merge/104637

Major rework of export plugin:

- export to PDF works now. You have to have packages pdflatex, pdftk and pdfjam
- icon shows again in the toolbar
- clean codebase, pylint compatible
- when exporting tasks finished last week, it shows only done tasks, not dismissed tasks (for status report I need list of accomplished things)

Looking forward to feedback!
-- 
https://code.launchpad.net/~izidor/gtg/export-rework/+merge/104637
Your team Gtg developers is requested to review the proposed merge of lp:~izidor/gtg/export-rework into lp:gtg.
=== modified file 'CHANGELOG'
--- CHANGELOG	2012-04-22 15:16:24 +0000
+++ CHANGELOG	2012-05-03 22:12:18 +0000
@@ -18,6 +18,7 @@
     * Reimplement the tag context menu as a widget, in order to - hopefully - be able to implement an enhanced context menu with, for instance, a color picker.
     * Background Colors option was moved from View menu into preferences dialog
     * Reorganised notification area menu (New task, Show browser, <tasks>, Quit)
+    * Improved Export plugin, export to PDF works now, by Izidor Matušov
 
 2012-02-13 Getting Things GNOME! 0.2.9
     * Big refractorization of code, now using liblarch

=== modified file 'GTG/plugins/export.gtg-plugin'
--- GTG/plugins/export.gtg-plugin	2012-03-05 15:23:05 +0000
+++ GTG/plugins/export.gtg-plugin	2012-05-03 22:12:18 +0000
@@ -5,7 +5,7 @@
 Description="""Exports the tasks in the current view into
 a variety of formats. You can also personalize the format
 of your tasks by writing your own template"""
-Authors=Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-Version=0.1.1
+Authors=Luca Invernizzi <invernizzi.l@xxxxxxxxx>, Izidor Matušov <izidor.matusov@xxxxxxxxx>
+Version=0.2
 Enabled=False
 Dependencies=Cheetah,

=== modified file 'GTG/plugins/export/__init__.py'
--- GTG/plugins/export/__init__.py	2012-03-05 15:23:05 +0000
+++ GTG/plugins/export/__init__.py	2012-05-03 22:12:18 +0000
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#               2012 - Izidor Matušov <izidor.matusov@xxxxxxxxx>
 #
 # This program is free software: you can redistribute it and/or modify it under
 # the terms of the GNU General Public License as published by the Free Software
@@ -14,8 +15,10 @@
 # You should have received a copy of the GNU General Public License along with
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from GTG.plugins.export.export import pluginExport
-
-
-#suppress pyflakes warning (given by make lint)
-if False == True: pluginExport()
+""" Initialize export pugin """
+
+from GTG.plugins.export.export import PluginExport
+
+# Make pyflakes happy
+if False:
+    PluginExport()

=== modified file 'GTG/plugins/export/export.py'
--- GTG/plugins/export/export.py	2012-04-01 12:40:28 +0000
+++ GTG/plugins/export/export.py	2012-05-03 22:12:18 +0000
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#               2012 - Izidor Matušov <izidor.matusov@xxxxxxxxx>
 #
 # This program is free software: you can redistribute it and/or modify it under
 # the terms of the GNU General Public License as published by the Free Software
@@ -14,329 +15,292 @@
 # You should have received a copy of the GNU General Public License along with
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 
+""" Export plugin
+Plugin for exporting into nice lists in TXT, HTML or PDF """
+
 import os
-import gtk
 import shutil
+import webbrowser
+
+from xdg.BaseDirectory import xdg_config_home
 import gobject
-import tempfile
-import threading
-import subprocess
-from Cheetah.Template  import Template as CheetahTemplate
-from xdg.BaseDirectory import xdg_config_home
-
-from GTG                          import _
-from GTG.plugins.export.task_str  import tree_to_TaskStr
-from GTG.plugins.export.templates import TemplateFactory
-from GTG.tools.logger             import Log
-
-
-
-class pluginExport:
-
+import gtk
+
+from GTG import _
+from GTG.plugins.export.task_str import get_task_wrappers
+from GTG.plugins.export.templates import Template, get_templates_paths
+
+
+def get_user_dir(key):
+    """
+    http://www.freedesktop.org/wiki/Software/xdg-user-dirs
+        XDG_DESKTOP_DIR
+        XDG_DOWNLOAD_DIR
+        XDG_TEMPLATES_DIR
+        XDG_PUBLICSHARE_DIR
+        XDG_DOCUMENTS_DIR
+        XDG_MUSIC_DIR
+        XDG_PICTURES_DIR
+        XDG_VIDEOS_DIR
+
+    Taken from FrontBringer
+    (distributed under the GNU GPL v3 license),
+    courtesy of Jean-François Fortin Tam.
+    """
+    user_dirs_dirs = os.path.join(xdg_config_home, "user-dirs.dirs")
+    user_dirs_dirs = os.path.expanduser(user_dirs_dirs)
+    if not os.path.exists(user_dirs_dirs):
+        return
+    for line in open(user_dirs_dirs, "r"):
+        if line.startswith(key):
+            return os.path.expandvars(line[len(key)+2:-2])
+
+
+def get_desktop_dir():
+    """ Returns path to desktop dir based on XDG.
+
+    If XDG is not setup corectly, use home directory instead """
+    desktop_dir = get_user_dir("XDG_DESKTOP_DIR")
+    if desktop_dir is not None and os.path.exists(desktop_dir):
+        return desktop_dir
+    else:
+        return os.path.expanduser('~')
+
+
+class PluginExport:
+    """ Export plugin - handle UI and trigger exporting tasks """
+
+    # Allow initilization outside __init__() and don't complain
+    # about too many attributes
+    # pylint: disable-msg=W0201,R0902
+
+    PLUGIN_NAME = "export"
+
+    DEFAULT_PREFERENCES = {
+        "menu_entry": True,
+        "toolbar_entry": True,
+    }
 
     def __init__(self):
-        '''Initialize all the GTK widgets'''
-        self.__init_gtk()
+        self.filename = None
+        self.template = None
 
     def activate(self, plugin_api):
-        '''loads the saved preferences'''
+        """ Loads saved preferences """
         self.plugin_api = plugin_api
-        self.preferences_load()
-        self.preferences_apply()
+        self._init_gtk()
+        self._preferences_load()
+        self._preferences_apply()
 
-    def deactivate(self, plugin_api):
-        '''Removes the gtk widgets before quitting'''
-        self.__gtk_deactivate()
+    def deactivate(self, plugin_api): # pylint: disable-msg=W0613
+        """ Removes the gtk widgets before quitting """
+        self._gtk_deactivate()
 
 ## CALLBACK AND CORE FUNCTIONS ################################################
-
-    def load_template(self):
-        self.template = TemplateFactory().create_template(\
-                                            self.combo_get_path(self.combo))
-        return self.template
-
-    def export_generate(self, document_ready):
-        #Template loading and cutting
+    def on_export_start(self, saving):
+        """ Start generating a document.
+        If saving == True, ask user where to store the document. Otherwise,
+        open it afterwards. """
+
+        model = self.combo.get_model()
+        active = self.combo.get_active()
+        self.template = Template(model[active][0])
+
+        self.filename = None
+        if saving:
+            self.filename = self.choose_file()
+            if self.filename is None:
+                return
+
+        self.save_button.set_sensitive(False)
+        self.open_button.set_sensitive(False)
+
+        try:
+            tasks = self.get_selected_tasks()
+            self.template.generate(tasks, self.plugin_api,
+                                    self.on_export_finished)
+        except Exception, err:
+            self.show_error_dialog(
+                        _("GTG could not generate the document: %s") % err)
+            raise
+
+    def on_export_finished(self):
+        """ Save generated file or open it, reenable buttons
+        and hide dialog """
+        document_path = self.template.get_document_path()
+        if document_path:
+            if self.filename:
+                shutil.copyfile(document_path, self.filename)
+            else:
+                webbrowser.open(document_path)
+        else:
+            self.show_error_dialog("Document creation failed. "
+                "Ensure you have all needed programs.")
+
+        self.save_button.set_sensitive(True)
+        self.open_button.set_sensitive(True)
+        self.export_dialog.hide()
+
+    def get_selected_tasks(self):
+        """ Filter tasks based on user option """
         timespan = None
+        req = self.plugin_api.get_requester()
+
         if self.export_all_active.get_active():
-            tree = self.plugin_api.get_requester().get_tasks_tree(name='active')
-            tree.apply_filter('active')
+            treename = 'active'
         elif self.export_all_finished.get_active():
-            tree = self.plugin_api.get_requester().get_tasks_tree(name='closed')
-            tree.apply_filter('closed')
+            treename = 'closed'
         elif self.export_finished_last_week.get_active():
-            tree = self.plugin_api.get_requester().get_tasks_tree(name='closed')
-            tree.apply_filter('closed')
+            treename = 'closed'
             timespan = -7
-        meta_root_node = tree.get_root()
-        root_nodes = [tree.get_node(c)
-                                        for c in meta_root_node.get_children()]
-        tasks_str = tree_to_TaskStr(tree, root_nodes, self.plugin_api, timespan)
-        document = str(CheetahTemplate(
-                                file = self.template.get_path(),
-                                searchList = [{'tasks':      tasks_str,
-                                               'plugin_api': self.plugin_api}]))
-        self.__purge_saved_document()
-        #we save the created document in a temporary file with the same suffix
-        #as the template (it's script-friendly)
-        with tempfile.NamedTemporaryFile(\
-                            suffix = ".%s" % self.template.get_suffix(),
-                            delete = False) as f:
-            f.write(document)
-            self.document_path = f.name
-        if self.template.get_script_path():
-            def __script_worker(self):
-                try:
-                    self.document_path = \
-                        subprocess.Popen(args = ['/bin/sh',
-                                             '-c',
-                                             self.template.get_script_path() + \
-                                             " " + self.document_path],
-                                            shell = False,
-                                            stdout = subprocess.PIPE\
-                                    ).communicate()[0]
-                except:
-                    pass
-                if self.document_path == "ERROR":
-                    Log.debug("Document creation failed")
-                    self.document_path = None
-                document_ready.set()
-            worker_thread = threading.Thread(
-                            target = __script_worker,
-                            args   = (self, ))
-            worker_thread.setDaemon(True)
-            worker_thread.start()
-        else:
-            document_ready.set()
-
-    def export_execute_with_ui(self, document_ready):
-        if not self.load_template():
-            self.show_error_dialog(_("Template not found"))
-            return False
-        #REMOVE ME
-        try:
-            self.export_generate(document_ready)
-        except Exception, e:
-            self.show_error_dialog( \
-                            _("GTG could not generate the document: %s") % e)
-            return False
-        return True
-
-    def on_export_open(self, widget = None, saving = False):
-        document_ready = threading.Event()
-        self.export_execute_with_ui(document_ready)
-        self.save_button.set_sensitive(False)
-        self.open_button.set_sensitive(False)
-        if saving:
-            filename = self.__get_filename_from_gtk_dialog()
-        else:
-            filename = None
-        def __wait_for_document_ready(self, document_ready, filename, saving):
-            document_ready.wait()
-            if filename:
-                if saving:
-                    shutil.copyfile(self.document_path, filename)
-            else:
-                subprocess.Popen(['xdg-open', self.document_path])
-            gobject.idle_add(self.save_button.set_sensitive, True)
-            gobject.idle_add(self.open_button.set_sensitive, True)
-
-        event_thread = threading.Thread( \
-                        target = __wait_for_document_ready,
-                        args = (self, document_ready, filename, saving))
-        event_thread.setDaemon(True)
-        event_thread.start()
-
-    def on_export_save(self, widget = None):
-        self.on_export_open(saving = True)
-
-    def hide(self):
-        self.__gtk_hide()
-        self.__purge_saved_document()
-
-    def __purge_saved_document(self):
-        try:
-            os.remove(self.document_path)
-        except:
-            pass
-
-## GTK FUNCTIONS #############################################################
-
-    def __init_gtk(self):
+
+        tree = req.get_tasks_tree(name=treename)
+        if treename not in tree.list_applied_filters():
+            tree.apply_filter(treename)
+
+        return get_task_wrappers(tree, timespan)
+
+
+## GTK FUNCTIONS ##############################################################
+    def _init_gtk(self):
+        """ Initialize all the GTK widgets """
         self.menu_entry = False
         self.toolbar_entry = False
-        self.path = os.path.dirname(os.path.abspath(__file__))
+
         self.menu_item = gtk.MenuItem(_("Export the tasks currently listed"))
-        self.menu_item.connect('activate', self.__gtk_activate)
+        self.menu_item.connect('activate', self.show_dialog)
+        self.menu_item.show()
+
         self.tb_button = gtk.ToolButton(gtk.STOCK_PRINT)
-        self.tb_button.connect('clicked', self.__gtk_activate)
-        self.builder = gtk.Builder()
-        self.builder.add_from_file(os.path.join(
-                                  os.path.dirname(os.path.abspath(__file__)) + \
-                                   "/export.ui"))
-        self.combo = self.builder.get_object("export_combo_templ")
-        self.export_dialog      = self.builder.get_object("export_dialog")
-        self.export_image       = self.builder.get_object("export_image")
-        self.preferences_dialog = self.builder.get_object("preferences_dialog")
-        self.pref_chbox_menu    = self.builder.get_object("pref_chbox_menu")
-        self.pref_chbox_toolbar = self.builder.get_object("pref_chbox_toolbar")
-        self.description_label  = self.builder.get_object("label_description")
-        self.save_button        = self.builder.get_object("export_btn_save")
-        self.open_button        = self.builder.get_object("export_btn_open")
-
-        self.export_all_active         = self.builder.get_object("export_all_active_rb")
-        self.export_finished_last_week = self.builder.get_object("export_finished_last_week_rb")
-        self.export_all_finished       = self.builder.get_object("export_all_finished_rb")
-
-        SIGNAL_CONNECTIONS_DIC = {
-            "on_export_btn_open_clicked": 
-                self.on_export_open,
-            "on_export_btn_save_clicked": 
-                self.on_export_save,
-            "on_export_dialog_delete_event": 
-                self.__gtk_hide,
+        self.tb_button.connect('clicked', self.show_dialog)
+        self.tb_button.show()
+
+        builder = gtk.Builder()
+        cur_dir = os.path.dirname(os.path.abspath(__file__))
+        builder_file = os.path.join(cur_dir, "export.ui")
+        builder.add_from_file(builder_file)
+
+        self.combo = builder.get_object("export_combo_templ")
+        templates_list = gtk.ListStore(gobject.TYPE_STRING,
+            gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
+        self.combo.set_model(templates_list)
+        cell = gtk.CellRendererText()
+        self.combo.pack_start(cell, True)
+        self.combo.add_attribute(cell, 'text', 1)
+
+        self.export_dialog = builder.get_object("export_dialog")
+        self.export_image = builder.get_object("export_image")
+        self.preferences_dialog = builder.get_object("preferences_dialog")
+        self.pref_menu = builder.get_object("pref_chbox_menu")
+        self.pref_toolbar = builder.get_object("pref_chbox_toolbar")
+        self.description_label = builder.get_object("label_description")
+        self.save_button = builder.get_object("export_btn_save")
+        self.open_button = builder.get_object("export_btn_open")
+
+        self.export_all_active = builder.get_object(
+                                                "export_all_active_rb")
+        self.export_finished_last_week = builder.get_object(
+                                                "export_finished_last_week_rb")
+        self.export_all_finished = builder.get_object(
+                                                "export_all_finished_rb")
+
+        builder.connect_signals({
+            "on_export_btn_open_clicked":
+                lambda widget: self.on_export_start(False),
+            "on_export_btn_save_clicked":
+                lambda widget: self.on_export_start(True),
+            "on_export_dialog_delete_event":
+                self._hide_dialog,
             "on_export_combo_templ_changed":
-                self.__on_combo_changed,
+                self.on_combo_changed,
             "on_preferences_dialog_delete_event":
                 self.on_preferences_cancel,
             "on_btn_preferences_cancel_clicked":
                 self.on_preferences_cancel,
             "on_btn_preferences_ok_clicked":
-                self.on_preferences_ok
-        }
-        self.builder.connect_signals(SIGNAL_CONNECTIONS_DIC)
-
-    def __gtk_activate(self, widget):
-        #Populating combo boxes
-        self.export_dialog.set_transient_for(\
-                                self.plugin_api.get_ui().get_window())
-        self.combo_decorator(self.combo, TemplateFactory().get_templates_paths())
+                self.on_preferences_ok,
+        })
+
+    def _gtk_deactivate(self):
+        """ Remove Toolbar Button and Menu item for this plugin """
+        if self.menu_entry:
+            self.plugin_api.remove_menu_item(self.menu_item)
+            self.menu_entry = False
+
+        if self.toolbar_entry:
+            self.plugin_api.remove_toolbar_item(self.tb_button)
+            self.toolbar_entry = False
+
+    def show_dialog(self, widget): # pylint: disable-msg=W0613
+        """ Show dialog with options for export """
+        parent_window = self.plugin_api.get_ui().get_window()
+        self.export_dialog.set_transient_for(parent_window)
+        self._update_combobox()
         self.export_dialog.show_all()
 
-    def __gtk_deactivate(self):
-        try:
-            self.plugin_api.remove_menu_item(self.menu_item)
-        except:
-            pass
-        self.menu_entry = False
-        try:
-            self.plugin_api.remove_toolbar_item(self.tb_button)
-        except:
-            pass
-        self.toolbar_entry = False
-
-    def __gtk_hide(self, sender = None, data = None):
+    def _hide_dialog(self, sender=None, data=None): # pylint: disable-msg=W0613
+        """ Hide dialog """
         self.export_dialog.hide()
         return True
 
-    def __on_combo_changed(self, widget = None):
-        if self.load_template():
-            image_path = self.template.get_image_path()
-            if image_path:
-                pixbuf = gtk.gdk.pixbuf_new_from_file(image_path)
-                [w,h] = self.export_image.get_size_request()
-                pixbuf = pixbuf.scale_simple(w, h, gtk.gdk.INTERP_BILINEAR)
-                self.export_image.set_from_pixbuf(pixbuf)
-            else:
-                self.export_image.clear()
-        description = self.template.get_description()
+    def _update_combobox(self):
+        """ Reload list of templates """
+        model = self.combo.get_model()
+        model.clear()
+
+        templates = get_templates_paths()
+        for path in templates:
+            template = Template(path)
+            model.append((path,
+                template.get_title(),
+                template.get_description(),
+                template.get_image_path()))
+
+        # wrap the combo-box if it's too long
+        if len(templates) > 15:
+            self.combo.set_wrap_width(5)
+        self.combo.set_active(0)
+
+    def on_combo_changed(self, combo):
+        """ Display details about the selected template """
+        model = combo.get_model()
+        active = combo.get_active()
+        if not 0 <= active < len(model):
+            return
+        description, image = model[active][2], model[active][3]
+
+        if image:
+            pixbuf = gtk.gdk.pixbuf_new_from_file(image)
+            width, height = self.export_image.get_size_request()
+            pixbuf = pixbuf.scale_simple(width, height,
+                                        gtk.gdk.INTERP_BILINEAR)
+            self.export_image.set_from_pixbuf(pixbuf)
+        else:
+            self.export_image.clear()
         self.description_label.set_markup("<i>%s</i>" % description)
 
-    def empty_tree_model(self, model):
-        if model == None:
-            return
-        iter = model.get_iter_first()
-        while iter:
-            this_iter =  iter
-            iter = model.iter_next(iter)
-            model.remove(this_iter)
-
-    def combo_list_store(self, list_store, a_list):
-        if list_store == None:
-            list_store = gtk.ListStore(gobject.TYPE_STRING,
-                                       gobject.TYPE_STRING)
-        self.empty_tree_model(list_store)
-        for template_path in a_list:
-            iter = list_store.append()
-            list_store.set(iter, 0,
-                 TemplateFactory().create_template(template_path).get_title())
-            list_store.set(iter, 1, template_path)
-        return self.export_list_store
-
-    def combo_completion(self, list_store):
-        completion = gtk.EntryCompletion()
-        completion.set_minimum_key_length(0)
-        completion.set_text_column(0)
-        completion.set_inline_completion(True)
-        completion.set_model(list_store)
-
-    def combo_set_text(self, combobox, entry):
-        model = combobox.get_model()
-        index = combobox.get_active()
-        if index > -1:
-            entry.set_text(model[index][0])
-
-    def combo_get_path(self, combobox):
-        model = combobox.get_model()
-        active = combobox.get_active()
-        if active < 0:
-            return None
-        return model[active][1]
-
-    def combo_decorator(self, combobox, a_list):
-        first_run = not hasattr(self, "combo_templ_entry")
-        if first_run:
-            self.combo_templ_entry = gtk.Entry()
-            combobox.add(self.combo_templ_entry)
-            self.export_list_store = gtk.ListStore(gobject.TYPE_STRING,
-                                                    gobject.TYPE_STRING)
-            self.combo_templ_entry.set_completion(
-                        self.combo_completion(self.export_list_store))
-            combobox.set_model(self.export_list_store)
-            combobox.connect('changed', self.combo_set_text,
-                         self.combo_templ_entry)
-            #render the combo-box drop down menu
-            cell = gtk.CellRendererText()
-            combobox.pack_start(cell, True)
-            combobox.add_attribute(cell, 'text', 0) 
-       #wrap the combo-box if it's too long
-        if len(a_list) > 15:
-            combobox.set_wrap_width(5)
-        #populate the combo-box
-        self.combo_list_store(self.export_list_store, a_list)
-        if not hasattr(self, "combo_active"):
-            self.combo_active = 0
-        combobox.set_active(self.combo_active)
-
     def show_error_dialog(self, message):
+        """ Display an error """
         dialog = gtk.MessageDialog(
-            parent         = self.export_dialog,
-            flags          = gtk.DIALOG_DESTROY_WITH_PARENT,
-            type           = gtk.MESSAGE_ERROR,
-            buttons        = gtk.BUTTONS_OK,
+            parent = self.export_dialog,
+            flags = gtk.DIALOG_DESTROY_WITH_PARENT,
+            type = gtk.MESSAGE_ERROR,
+            buttons = gtk.BUTTONS_OK,
             message_format = message)
-        dialog.run() 
+        dialog.run()
         dialog.destroy()
 
-    def __get_filename_from_gtk_dialog(self):
-        chooser = gtk.FileChooserDialog(\
+    def choose_file(self):
+        """ Let user choose a file to save and return its path """
+        chooser = gtk.FileChooserDialog(
                 title = _("Choose where to save your list"),
                 parent = self.export_dialog,
                 action = gtk.FILE_CHOOSER_ACTION_SAVE,
-                buttons = (gtk.STOCK_CANCEL,
-                           gtk.RESPONSE_CANCEL,
-                           gtk.STOCK_SAVE,
-                           gtk.RESPONSE_OK))
+                buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                           gtk.STOCK_SAVE, gtk.RESPONSE_OK))
         chooser.set_do_overwrite_confirmation(True)
-        desktop_dir = self.get_user_dir("XDG_DESKTOP_DIR")
-        #NOTE: using ./scripts/debug.sh, it doesn't detect the Desktop
-        # dir, as the XDG directories are changed. That is why during 
-        # debug it defaults to the Home directory ~~Invernizzi~~
-        if desktop_dir != None and os.path.exists(desktop_dir):
-            chooser.set_current_folder(desktop_dir)
-        else:
-            chooser.set_current_folder(os.environ['HOME'])
         chooser.set_default_response(gtk.RESPONSE_OK)
+        chooser.set_current_folder(get_desktop_dir())
         response = chooser.run()
         filename = chooser.get_filename()
         chooser.destroy()
@@ -345,84 +309,61 @@
         else:
             return None
 
-## Helper methods ############################################################
-
-    def get_user_dir(self, key):
-        """
-        http://www.freedesktop.org/wiki/Software/xdg-user-dirs
-            XDG_DESKTOP_DIR
-            XDG_DOWNLOAD_DIR
-            XDG_TEMPLATES_DIR
-            XDG_PUBLICSHARE_DIR
-            XDG_DOCUMENTS_DIR
-            XDG_MUSIC_DIR
-            XDG_PICTURES_DIR
-            XDG_VIDEOS_DIR
-
-        Taken from FrontBringer
-        (distributed under the GNU GPL v3 license),
-        courtesy of Jean-François Fortin Tam.
-        """
-        user_dirs_dirs = os.path.expanduser(xdg_config_home + "/user-dirs.dirs")
-        if os.path.exists(user_dirs_dirs):
-            f = open(user_dirs_dirs, "r")
-            for line in f.readlines():
-                if line.startswith(key):
-                    return os.path.expandvars(line[len(key)+2:-2])
-
-## Preferences methods #########################################################
-
-    DEFAULT_PREFERENCES = {"menu_entry":     True,
-                            "toolbar_entry": True}
-    PLUGIN_NAME = "export"
-
-    def is_configurable(self):
+## Preferences methods ########################################################
+    @classmethod
+    def is_configurable(cls):
         """A configurable plugin should have this method and return True"""
         return True
 
     def configure_dialog(self, manager_dialog):
-        self.preferences_load()
+        """ Display configuration dialog """
+        self._preferences_load()
         self.preferences_dialog.set_transient_for(manager_dialog)
-        self.pref_chbox_menu.set_active(self.preferences["menu_entry"])
-        self.pref_chbox_toolbar.set_active(self.preferences["toolbar_entry"])
+        self.pref_menu.set_active(self.preferences["menu_entry"])
+        self.pref_toolbar.set_active(self.preferences["toolbar_entry"])
         self.preferences_dialog.show_all()
 
-    def on_preferences_cancel(self, widget = None, data = None):
+    def on_preferences_cancel(self, widget, data=None):
+        # pylint: disable-msg=W0613
+        """ Only hide the dialog """
         self.preferences_dialog.hide()
         return True
 
-    def on_preferences_ok(self, widget = None, data = None):
-        self.preferences["menu_entry"] = self.pref_chbox_menu.get_active()
-        self.preferences["toolbar_entry"] = self.pref_chbox_toolbar.get_active()
-        self.preferences_apply()
-        self.preferences_store()
+    def on_preferences_ok(self, widget): # pylint: disable-msg=W0613
+        """ Apply and store new preferences """
+        self.preferences["menu_entry"] = self.pref_menu.get_active()
+        self.preferences["toolbar_entry"] = self.pref_toolbar.get_active()
         self.preferences_dialog.hide()
 
-    def preferences_load(self):
-        data = self.plugin_api.load_configuration_object(self.PLUGIN_NAME,\
-                                                         "preferences")
-        if data == None or type(data) != type (dict()):
+        self._preferences_apply()
+        self._preferences_store()
+
+    def _preferences_load(self):
+        """ Restore user preferences """
+        data = self.plugin_api.load_configuration_object(
+                            self.PLUGIN_NAME, "preferences")
+        if type(data) != type(dict()):
             self.preferences = self.DEFAULT_PREFERENCES
         else:
             self.preferences = data
 
-    def preferences_store(self):
-        self.plugin_api.save_configuration_object(self.PLUGIN_NAME,\
-                                                  "preferences", \
-                                                  self.preferences)
+    def _preferences_store(self):
+        """ Store user preferences """
+        self.plugin_api.save_configuration_object(
+            self.PLUGIN_NAME, "preferences", self.preferences)
 
-    def preferences_apply(self):
-        if self.preferences["menu_entry"] and self.menu_entry == False:
+    def _preferences_apply(self):
+        """ Add/remove menu entry/toolbar entry """
+        if self.preferences["menu_entry"] and not self.menu_entry:
             self.plugin_api.add_menu_item(self.menu_item)
             self.menu_entry = True
-        elif self.preferences["menu_entry"]==False and self.menu_entry == True:
+        elif not self.preferences["menu_entry"] and self.menu_entry:
             self.plugin_api.remove_menu_item(self.menu_item)
             self.menu_entry = False
 
-        if self.preferences["toolbar_entry"] and self.toolbar_entry == False:
+        if self.preferences["toolbar_entry"] and not self.toolbar_entry:
             self.plugin_api.add_toolbar_item(self.tb_button)
             self.toolbar_entry = True
-        elif self.preferences["toolbar_entry"]==False and self.toolbar_entry == True:
+        elif not self.preferences["toolbar_entry"] and self.toolbar_entry:
             self.plugin_api.remove_toolbar_item(self.tb_button)
             self.toolbar_entry = False
-

=== modified file 'GTG/plugins/export/export_templates/description_pocketmod.py'
--- GTG/plugins/export/export_templates/description_pocketmod.py	2012-03-05 15:23:05 +0000
+++ GTG/plugins/export/export_templates/description_pocketmod.py	2012-05-03 22:12:18 +0000
@@ -16,6 +16,7 @@
 
 from GTG import _
 title = _("Foldable booklet (PDF)")
-description = _("A template to create a <a"
-                " href=\"http://www.pocketmod.com\";>PocketMod</a>, which is a "
-                " small foldable booklet.")
+description = _("""A template to create
+<a href="http://www.pocketmod.com";>PocketMod</a>, which is a small foldable
+booklet. Packages <b>pdflatex</b>, <b>pdftk</b> and <b>pdfjam</b>
+are required.""")

=== modified file 'GTG/plugins/export/export_templates/description_textual.py'
--- GTG/plugins/export/export_templates/description_textual.py	2012-03-05 15:23:05 +0000
+++ GTG/plugins/export/export_templates/description_textual.py	2012-05-03 22:12:18 +0000
@@ -14,6 +14,9 @@
 # You should have received a copy of the GNU General Public License along with
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 
+""" Text only template """
+
 from GTG import _
+
 title = _("Text-only")
 description = _("A template to create a simple text file with some tasks.")

=== modified file 'GTG/plugins/export/export_templates/script_pocketmod'
--- GTG/plugins/export/export_templates/script_pocketmod	2011-11-19 21:41:08 +0000
+++ GTG/plugins/export/export_templates/script_pocketmod	2012-05-03 22:12:18 +0000
@@ -1,11 +1,25 @@
 #/bin/sh
 #
-#Script to create a pocketmod form a latex source file
-#Author: Jan Girlich <vollkorn@xxxxxxxxxx>,
-#        Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-#Note that the GTG export plugin passes the script the path of the source file
-#and expects to be given the path of the generated file in the standard output
+# Copyright (c) 2009 - Jan Girlich <vollkorn@xxxxxxxxxx>, Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Script to create a pocketmod form a latex source file
+#
+# Note that the GTG export plugin passes the script the path of the source file
+# and expects to be given the path of the generated file in the standard output
+
 TMPFOLDER=`mktemp -d /tmp/pocketmod.XXXXXXXXXX`
 OUTFILE=`mktemp /tmp/pockemod.XXXXXXXXXXXXX`.pdf
 SOURCEFILE=$1
@@ -16,21 +30,6 @@
 pdf90 $TMPFOLDER/seite5432.pdf --outfile $TMPFOLDER/seite5432-r.pdf      1>&2
 pdf90 $TMPFOLDER/seite5432-r.pdf --outfile $TMPFOLDER/seite5432-r2.pdf      1>&2
 pdftk $TMPFOLDER/seite6781.pdf $TMPFOLDER/seite5432-r2.pdf cat output $TMPFOLDER/gesamt.pdf      1>&2
-pdfnup $TMPFOLDER/gesamt.pdf --nup 4x2 --orient landscape --outfile $OUTFILE      1>&2
+pdfnup $TMPFOLDER/gesamt.pdf --nup 4x2 --landscape --outfile $OUTFILE      1>&2
 #rm -rf $TMPFOLDER      1>&2
 echo -n $OUTFILE
-# -*- coding: utf-8 -*-
-# Copyright (c) 2009 - Jan Girlich <vollkorn@xxxxxxxxxx>, Luca Invernizzi <invernizzi.l@xxxxxxxxx>
-#
-# This program is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free Software
-# Foundation, either version 3 of the License, or (at your option) any later
-# version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-# details.
-#
-# You should have received a copy of the GNU General Public License along with
-# this program.  If not, see <http://www.gnu.org/licenses/>.

=== modified file 'GTG/plugins/export/export_templates/template_pocketmod.tex'
--- GTG/plugins/export/export_templates/template_pocketmod.tex	2012-03-05 15:23:05 +0000
+++ GTG/plugins/export/export_templates/template_pocketmod.tex	2012-05-03 22:12:18 +0000
@@ -20,10 +20,10 @@
 #def task_template($task)
 \item[\Square]
 	#if $task.has_title
-	\textbf{$task.title}
+	\textbf{$task.title.replace('_', '\_')}
 		#if $task.has_tags
 			#for $tag in $task.tags:
-				\textit{$tag}
+				\textit{$tag.replace('_', '\_')}
 			#end for
 		#end if
 	#end if

=== modified file 'GTG/plugins/export/task_str.py'
--- GTG/plugins/export/task_str.py	2012-03-17 02:20:46 +0000
+++ GTG/plugins/export/task_str.py	2012-05-03 22:12:18 +0000
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#               2012 - Izidor Matušov <izidor.matusov@xxxxxxxxx>
 #
 # This program is free software: you can redistribute it and/or modify it under
 # the terms of the GNU General Public License as published by the Free Software
@@ -14,35 +15,29 @@
 # You should have received a copy of the GNU General Public License along with
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 
-class TaskStr:
-    '''
-    This class is a wrapper around the classic GTG.core.task.Task. It provides
-    access to the task various attributes directly via python attributes
-    instead of method calls. This makes writing Cheetah templates easier
-    '''
-
-    def __init__(self,
-                 title,
-                 text,
-                 subtasks,
-                 status,
-                 modified,
-                 due_date,
-                 closed_date,
-                 start_date,
-                 days_left,
-                 tags,
-                ):
-        self.title         = title
-        self.text          = text
-        self.subtasks      = subtasks
-        self.status        = status
-        self.modified      = modified
-        self.due_date      = due_date
-        self.closed_date   = closed_date
-        self.start_date    = start_date
-        self.days_left     = days_left
-        self.tags          = tags
+""" Text representation of GTG task for easier work in templates """
+
+
+class TaskStr(object):
+    """ Wrapper around GTG Task.
+    It provides access to the task various attributes directly via python
+    attributes instead of method calls and makes writing Cheetah
+    templates easier. """
+    # Ignore big number of properties and small number of public methods
+    # pylint: disable-msg=R0902,R0903
+    def __init__(self, task, subtasks):
+        self.title = task.get_title()
+        self.text = str(task.get_text())
+        self.status = task.get_status()
+        self.modified = str(task.get_modified_string())
+        self.due_date = str(task.get_due_date())
+        self.closed_date = str(task.get_closed_date())
+        self.start_date = str(task.get_start_date())
+        self.days_left = str(task.get_days_left())
+        self.tags = [t.get_id() for t in task.get_tags()]
+
+        self.subtasks = subtasks
+
     has_title         = property(lambda s: s.title       != "")
     has_text          = property(lambda s: s.text        != "")
     has_subtasks      = property(lambda s: s.subtasks    != [])
@@ -54,51 +49,40 @@
     has_days_left     = property(lambda s: s.days_left   != "")
     has_tags          = property(lambda s: s.tags        != [])
 
-def TaskStr_factory(task):
-    '''
-    Creates a TaskStr object given a gtg task
-    '''
-    return TaskStr(title = task.get_title(),
-                   text        = str(task.get_text()),
-                   subtasks    = [],
-                   status      = task.get_status(),
-                   modified    = str(task.get_modified_string()),
-                   due_date    = str(task.get_due_date()),
-                   closed_date = str(task.get_closed_date()),
-                   start_date  = str(task.get_start_date()),
-                   days_left   = str(task.get_days_left()),
-                   tags        = [t.get_name() for t in task.get_tags()])
-
-def tree_to_TaskStr(tree, nodes, plugin_api, days = None):
-    """This function performs a depth-first tree visits on a tree 
-        using the given nodes as root. For each node of the tree it
-        encounters, it generates a TaskStr object and returns that.
-        The resulting TaskStr will be linked to its subtasks in the
-        same way as the tree"""
-    tasks_str = []
-    for node in nodes:
-        if not tree.is_displayed(node.get_id()):
-            continue
-        task = plugin_api.get_requester().get_task(node.get_id())
-        #The task_str is added to the result only if it satisfies the time
-        # limit imposed with the @days parameter of this function
-        if days and not _is_task_in_timespan(task, days):
-            continue
-        task_str = TaskStr_factory(task)
-        tasks_str.append(task_str)
-        if node.has_child():
-            children = [tree.get_node(c) for c in node.get_children()]
-            task_str.subtasks = tree_to_TaskStr(tree,
-                                                children,
-                                                plugin_api,
-                                                days)
-    return tasks_str
-
-def _is_task_in_timespan(task, days):
-    '''If days < 0, returns True if the task has been closed in the last
-    #abs(days). If days >= 0, returns True if the task is due in the next
-    #days'''
-    return (days < 0 and task.get_closed_date() and \
-                (task.get_closed_date().days_left() >= days)) or \
-           (days >= 0 and (task.get_days_left() <= days))
-
+
+def get_task_wrappers(tree, days = None, task_id = None):
+    """ Recursively find all task on given tree and
+    convert them into TaskStr
+
+    tree - tree of tasks
+    days - filter days in certain timespan
+    task_id - return subtasks of this tasks. If not set, use root node """
+
+    def _is_in_timespan(task):
+        """ Return True if days is not set.
+        If days < 0, returns True if the task has been done in the last
+        #abs(days).
+        If days >= 0, returns True if the task is due in the next #days """
+        if days is None:
+            return True
+        elif days < 0:
+            done = task.get_status() == task.STA_DONE
+            closed_date = task.get_closed_date()
+            return done and closed_date and closed_date.days_left() >= days
+        else:
+            return task.get_days_left() <= days
+
+    subtasks = []
+    for sub_id in tree.node_all_children(task_id):
+        subtask = get_task_wrappers(tree, days, sub_id)
+        if subtask is not None:
+            subtasks.append(subtask)
+
+    if task_id is None:
+        return subtasks
+    else:
+        task = tree.get_node(task_id)
+        if task is None or not _is_in_timespan(task):
+            return None
+
+        return TaskStr(task, subtasks)

=== modified file 'GTG/plugins/export/templates.py'
--- GTG/plugins/export/templates.py	2012-03-17 02:20:46 +0000
+++ GTG/plugins/export/templates.py	2012-05-03 22:12:18 +0000
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2010 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#               2012 - Izidor Matušov <izidor.matusov@xxxxxxxxx>
 #
 # This program is free software: you can redistribute it and/or modify it under
 # the terms of the GNU General Public License as published by the Free Software
@@ -14,109 +15,150 @@
 # You should have received a copy of the GNU General Public License along with
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 
+""" Module for discovering templates and work with templates """
+
+from glob import glob
+import os.path
+import subprocess
 import sys
-import glob
-import os.path
+import tempfile
+import threading
+
+from Cheetah.Template  import Template as CheetahTemplate
 from xdg.BaseDirectory import xdg_config_home
-
-from GTG.tools.borg import Borg
-
-
-class TemplateFactory(Borg):
-    '''
-    A Factory which provides an easy access to the templates. It caches
-    templates for the sake of speed.
-    '''
-
-    TEMPLATE_PATHS = [\
-            os.path.join(xdg_config_home,
-                         "gtg/plugins/export/export_templates"),
-            os.path.join(os.path.dirname(os.path.abspath(__file__)),
-                         "export_templates/")]
-
-    def __init__(self):
-        super(TemplateFactory, self).__init__()
-        if hasattr(self, "_cache"):
-            #The borg has already been initialized
-            return
-        self._cache = {}
-
-    @classmethod
-    def get_templates_paths(cls):
-        '''
-        Returns a list containing the full path for all the
-        available templates
-        '''
-        template_list = []
-        for a_dir in TemplateFactory.TEMPLATE_PATHS:
-            template_list += glob.glob(os.path.join(a_dir, "template_*"))
-        return template_list
-
-    def create_template(self, path):
+import gobject
+
+TEMPLATE_PATHS = [
+ os.path.join(xdg_config_home, "gtg/plugins/export/export_templates"),
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "export_templates"),
+]
+
+
+def get_templates_paths():
+    """ Returns a list containing the full path for all the
+    available templates. """
+    template_list = []
+    for a_dir in TEMPLATE_PATHS:
+        template_list += glob(os.path.join(a_dir, "template_*"))
+    return template_list
+
+
+class Template:
+    """ Representation of a template """
+
+    def __init__(self, path):
+        self._template = path
+        self._document_path = None
+
+        self._image_path = self._find_file("thumbnail_")
+        self._script_path = self._find_file("script_")
+
+        self._title, self._description = self._load_description()
+
+    def _find_file(self, prefix, suffix = ""):
+        """ Find a file for the template given prefix and suffix """
+        basename = os.path.basename(self._template)
+        basename = basename.replace("template_", prefix)
+        path = os.path.join(os.path.dirname(self._template), basename)
+        path = os.path.splitext(path)[0] + '*' + suffix
+        possible_filles = glob(path)
+        if len(possible_filles) > 0:
+            return possible_filles[0]
+        else:
+            return None
+
+    def _load_description(self):
+        """ Returns title and description of the template
+        template description are stored in python module for easier l10n.
+        thus, we need to import the module given its path """
+        path = self._find_file("description_", ".py")
         if not path:
-            return None
-        if not path in self._cache:
-            self._cache[path] = Template(path)
-        return self._cache[path]
-
-
-
-class Template(object):
-
-
-    def __init__(self, path):
-        self.__template_path = path
-        self.__image_path = self.__find_template_file(path, "thumbnail_")
-        self.__script_path = self.__find_template_file(path, "script_")
-        self.__description = ""
-        self.__title = ""
-        description_path = \
-                self.__find_template_file(path, "description_", ".py")
-        if description_path:
-            #template description are stored in python module for easier l10n.
-            #thus, we need to import the module given its path
-            directory_path= os.path.dirname(description_path)
-            if directory_path not in sys.path:
-                sys.path.append(directory_path)
-            module_name = os.path.basename(\
-                            description_path).replace(".py", "")
-            try:
-                module = __import__(module_name,
-                                    globals(),
-                                    locals(),
-                                    ['description'],
-                                    0)
-                self.__description =  module.description
-                self.__title =  module.title
-            except (ImportError, AttributeError):
-                pass
-
-    @classmethod
-    def __find_template_file(cls, path, prefix, suffix = ""):
-        basename = os.path.basename(path).replace("template_", prefix)
-        path = os.path.join(os.path.dirname(path), basename)
-                            
-        path = "%s*" % path[: path.rindex(".") - 1]
+            return "", ""
+        dir_path = os.path.dirname(path)
+        if dir_path not in sys.path:
+            sys.path.append(dir_path)
+        module_name = os.path.basename(path).replace(".py", "")
         try:
-            possible_paths = glob.glob(path)
-            return filter(lambda p: p.endswith(suffix), possible_paths)[0]
-        except:
-            return None
+            module = __import__(module_name, globals(), locals(),
+                                ['description'], 0)
+            return module.title, module.description
+        except (ImportError, AttributeError):
+            return "", ""
+
+    def _get_suffix(self):
+        """ Return suffix of the template """
+        return os.path.splitext(self._template)[1]
 
     def get_path(self):
-        return self.__template_path
+        """ Return path to the template """
+        return self._template
 
     def get_image_path(self):
-        return self.__image_path
-
-    def get_script_path(self):
-        return self.__script_path
-
-    def get_suffix(self):
-        return self.__template_path[self.__template_path.rindex(".") +1 :]
+        """ Return path to the image """
+        return self._image_path
+
+    def get_title(self):
+        """ Return title of the template """
+        return self._title
 
     def get_description(self):
-        return self.__description
-
-    def get_title(self):
-        return self.__title
+        """ Return description of the template """
+        return self._description
+
+    def get_document_path(self):
+        """ Return path to generated document.
+        Return None until generate() was successful."""
+        return self._document_path
+
+    def generate(self, tasks, plugin_api, callback):
+        """ Fill template and run callback when finished.
+
+        Created files are saved with the same suffix as the template. Opening
+        the final file determines its type based on suffix. """
+        document = CheetahTemplate(file = self.get_path(),
+            searchList = [{'tasks': tasks, 'plugin_api': plugin_api}])
+
+        suffix = ".%s" % self._get_suffix()
+        output = tempfile.NamedTemporaryFile(suffix = suffix, delete = False)
+        output.write(str(document))
+        self._document_path = output.name
+        output.close()
+
+        if self._script_path:
+            self._run_script(callback)
+        else:
+            callback()
+
+    def _run_script(self, callback):
+        """ Run script in its own thread and in other thread wait
+        for the result. """
+        document_ready = threading.Event()
+
+        def script():
+            """ Run script using /bin/sh.
+
+            The script gets path to a document as it only argument and
+            this thread expects resulting file as the only output of
+            the script. """
+
+            cmd = self._script_path + " " + self._document_path
+            self._document_path = None
+            try:
+                self._document_path = subprocess.Popen(
+                    args = ['/bin/sh', '-c', cmd],
+                    shell = False, stdout = subprocess.PIPE)\
+                                            .communicate()[0]
+            except Exception: # pylint: disable-msg=W0703
+                pass
+
+            if self._document_path and not os.path.exists(self._document_path):
+                self._document_path = None
+            document_ready.set()
+
+        def wait_for_document():
+            """ Wait for the completion of the script and finish generation """
+            document_ready.wait()
+            gobject.idle_add(callback)
+
+        threading.Thread(target = script).start()
+        threading.Thread(target = wait_for_document).start()