gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #02463
[Merge] lp:~gtg-user/gtg/backends-first-merge into lp:gtg
Luca Invernizzi has proposed merging lp:~gtg-user/gtg/backends-first-merge into lp:gtg.
Requested reviews:
Gtg developers (gtg)
This branch contains a series of things. I wanted to do separate merges, but unfortunately bazaar does not support partial merges, so it would have been a merge hell on my next merge.
- refactored signals for deleting a task. Now no phantoms are left if you delete. The FilteredTree emits "path-deleted" signals to update the task browser.
- new datastore with testset. It should have most of the features that I will need for the multi-backend feature
- refatored loading and saving backends
--
https://code.launchpad.net/~gtg-user/gtg/backends-first-merge/+merge/26433
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/backends-first-merge into lp:gtg.
=== modified file 'GTG/backends/__init__.py'
--- GTG/backends/__init__.py 2010-03-01 01:55:12 +0000
+++ GTG/backends/__init__.py 2010-05-31 15:39:30 +0000
@@ -23,14 +23,130 @@
(like on the hard disk or on the internet)
and to read projects from this medium
"""
-#
-#Current backends are :
-#
-# localfile.py : store and read a local XML file
-#
-#
-#
-# this __init__.py should not be empty. It should list the available backends
-#
-#http://www.faqts.com/knowledge_base/view.phtml/aid/4221/fid/538
-#http://www.python.org/doc/2.1.3/tut/node8.html
+
+import sys
+import uuid
+import os.path
+
+from GTG.tools.logger import Log
+from GTG.tools.borg import Borg
+from GTG.backends.genericbackend import GenericBackend
+
+
+
+class BackendFactory(Borg):
+ '''
+ This class holds the information about the backend types.
+ Since it's about types, all information is static. The instantiated
+ backends are handled in the Datastore.
+ It is a Borg for what matters its only state (_backend_modules),
+ since it makes no sense of keeping multiple instances of this.
+ '''
+
+
+ BACKEND_PREFIX = "backend_"
+
+ def __init__(self):
+ """
+ Creates a dictionary of the currently available backend modules
+ """
+ super(BackendFactory, self).__init__()
+ if hasattr(self, "backend_modules"):
+ #This object has already been constructed
+ return
+ self.backend_modules = {}
+ #Look for backends in the GTG/backends dir
+ this_dir = os.path.dirname(__file__)
+ backend_files = filter(lambda f: f.endswith(".py") and \
+ f[ : len(self.BACKEND_PREFIX)] == self.BACKEND_PREFIX , \
+ os.listdir(this_dir))
+ #Create module names
+ module_names = map(lambda f: f.replace(".py",""), backend_files)
+ Log.debug("Backends found: " + str(module_names))
+ #Load backend modules
+ for module_name in module_names:
+ extended_module_name = "GTG.backends." + module_name
+ try:
+ __import__(extended_module_name)
+ except ImportError, exception:
+ #Something is wrong with this backend, skipping
+ Log.debug("Backend %s could not be loaded: %s" % \
+ (module_name, str(exception)))
+ continue
+ self.backend_modules[module_name] = sys.modules[extended_module_name]
+
+ def get_backend(self, backend_name):
+ '''
+ Returns the backend module for the backend matching
+ backend_name. Else, returns none
+ '''
+ if backend_name in self.backend_modules:
+ return self.backend_modules[backend_name]
+ else:
+ Log.debug("Trying to load backend %s, but failed!" % backend_name)
+ return None
+
+ def get_all_backends(self):
+ '''
+ Returns a dictionary containing all the backends types
+ '''
+ return self.backend_modules
+
+ def get_new_backend_dict(self, backend_name):
+ '''
+ Constructs a new backend initialization dictionary. In more
+ exact terms, creates a dictionary, containing all the necessary
+ entries to initialize a backend.
+ '''
+ if not self.backend_modules.has_key(backend_name):
+ return None
+ dic = {}
+ module = self.get_backend(backend_name)
+ #Different pids are necessary to discern between backends of the same
+ # type
+ dic["pid"] = str(uuid.uuid4())
+ dic["module"] = module.Backend.get_name()
+ parameters = module.Backend.get_static_parameters()
+ #we all the parameters and their default values in dic
+ for param_name, param_dic in parameters.iteritems():
+ #FIXME: add type handling
+ dic[param_name] = param_dic[GenericBackend.PARAM_DEFAULT_VALUE]
+ dic["backend"] = module.Backend(dic)
+ print dic
+ return dic
+
+ def restore_backend_from_xml(self, dic):
+ '''
+ Function restoring a backend from its xml description.
+ dic should be a dictionary containing at least the key
+ - "module", with the module name
+ - "xmlobject", with its xml description.
+ Every other key is passed as-is to the backend, as parameter.
+
+ Returns the backend instance, or None is something goes wrong
+ '''
+ if not "module" in dic or not "xmlobject" in dic:
+ Log.debug ("Malformed backend configuration found! %s" % \
+ dic)
+ module = self.get_backend(dic["module"])
+ if module == None:
+ Log.debug ("could not load module for backend %s" % \
+ dic["module"])
+ return None
+ #we pop the xml object, as it will be redundant when the parameters
+ # are set directly in the dict
+ xp = dic.pop("xmlobject")
+ #Building the dictionary
+ parameters_specs = module.Backend.get_static_parameters()
+ dic["pid"] = str(xp.getAttribute("pid"))
+ for param_name, param_dic in parameters_specs.iteritems():
+ if xp.hasAttribute(param_name):
+ #we need to convert the parameter to the right format.
+ # we fetch the format from the static_parameters
+ param_type = param_dic[GenericBackend.PARAM_TYPE]
+ param_value = GenericBackend.cast_param_type_from_string( \
+ xp.getAttribute(param_name), param_type)
+ dic[param_name] = param_value
+ #We put the backend itself in the dict
+ dic["backend"] = module.Backend(dic)
+ return dic["backend"]
=== added file 'GTG/backends/backend_localfile.py'
--- GTG/backends/backend_localfile.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_localfile.py 2010-05-31 15:39:30 +0000
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+'''
+Localfile is a read/write backend that will store your tasks in an XML file
+This file will be in your $XDG_DATA_DIR/gtg folder.
+'''
+
+import os
+import uuid
+from collections import deque
+import threading
+
+from GTG.backends.genericbackend import GenericBackend
+from GTG.core import CoreConfig
+from GTG.tools import cleanxml, taskxml
+from GTG import _
+from GTG.tools.logger import Log
+
+
+
+class Backend(GenericBackend):
+
+
+ DEFAULT_PATH = CoreConfig.DATA_DIR #default path for filenames
+
+
+ #Description of the backend (mainly it's data we show the user, only the
+ # name is used internally. Please note that BACKEND_NAME and
+ # BACKEND_ICON_NAME should *not* be translated.
+ _general_description = { \
+ GenericBackend.BACKEND_NAME: "backend_localfile", \
+ GenericBackend.BACKEND_HUMAN_NAME: _("Local File"), \
+ GenericBackend.BACKEND_ICON_NAME: "backend_local_file", \
+ GenericBackend.BACKEND_AUTHORS: ["Lionel Dricot", \
+ "Luca Invernizzi"], \
+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
+ GenericBackend.BACKEND_DESCRIPTION: \
+ _("Your tasks are saved in a text file (XML format). " + \
+ " This is the most basic and the default way " + \
+ "for GTG to save your tasks."),\
+ }
+
+ #parameters to configure a new backend of this type.
+ #NOTE: should we always give back a different default filename? it can be
+ # done, but I'd like to keep this backend simple, so that it can be
+ # used as example (invernizzi)
+ _static_parameters = { \
+ "path": { \
+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_FILENAME, \
+ GenericBackend.PARAM_CONFIGURABLE: True, \
+ GenericBackend.PARAM_DEFAULT_VALUE: \
+ os.path.join(DEFAULT_PATH, "gtg_tasks-%s.xml" %(uuid.uuid4()))
+ }}
+
+ def _get_default_filename_path(self, filename = None):
+ '''
+ Generates a default path with a random filename
+ @param filename: specify a filename
+ '''
+ if not filename:
+ filename = "gtg_tasks-%s.xml" % (uuid.uuid4())
+ return os.path.join(self.DEFAULT_PATH, filename)
+
+ def __init__(self, parameters):
+ """
+ Instantiates a new backend.
+
+ @param parameters: should match the dictionary returned in
+ get_parameters. Anyway, the backend should care if one expected
+ value is None or does not exist in the dictionary.
+ @firstrun: only needed for the default backend. It should be
+ omitted for all other backends.
+ """
+ super(Backend, self).__init__(parameters)
+ self.tids = []
+ self.to_set_timer = None
+ self.to_set = deque()
+ self.to_remove = deque()
+
+
+ #####RETROCOMPATIBILIY
+ #NOTE: retrocompatibility. We convert "filename" to "path"
+ # and we forget about "filename"
+ if "need_conversion" in parameters:
+ parameters["path"] = os.path.join(self.DEFAULT_PATH, \
+ parameters["need_conversion"])
+ del parameters["need_conversion"]
+ if not self.KEY_DEFAULT_BACKEND in parameters:
+ parameters[self.KEY_DEFAULT_BACKEND] = True
+ ####
+
+ #To be extra sure, if we are the default backend and we don't have a
+ # path set, we generate one (shouldn't happen)
+ if parameters[self.KEY_DEFAULT_BACKEND] and \
+ "path" not in parameters:
+ parameters["path"] = self._get_default_filename_path()
+ Log.debug("Path for default backend has not been found. Please" +\
+ "report this bug")
+ self.doc, self.xmlproj = cleanxml.openxmlfile( \
+ parameters["path"], "project")
+
+ def this_is_the_first_run(self, xml):
+ #Create the default tasks for the first run.
+ #We write the XML object in a file
+ self._parameters[self.KEY_DEFAULT_BACKEND] = True
+ cleanxml.savexml(self._parameters["path"], xml)
+ self.doc, self.xmlproj = cleanxml.openxmlfile(\
+ self._parameters["path"], "project")
+ self._parameters[self.KEY_DEFAULT_BACKEND] = True
+
+ def start_get_tasks(self,push_task_func,task_factory_func):
+ '''
+ Once this function is launched, the backend can start pushing
+ tasks to gtg parameters.
+
+ @push_task_func: a function that takes a Task as parameter
+ and pushes it into GTG.
+ @task_factory_func: a function that takes a tid as parameter
+ and returns a Task object with the given tid.
+
+ @return: start_get_tasks() might not return or finish
+ '''
+ tid_list = []
+ for node in self.xmlproj.childNodes:
+ tid = node.getAttribute("id")
+ if tid not in self.tids:
+ self.tids.append(tid)
+ task = task_factory_func(tid)
+ if task:
+ task = taskxml.task_from_xml(task, node)
+ push_task_func(task)
+ else:
+ print "tried to load task with the same tid"
+ #print "#### finishing pushing tasks"
+
+ def set_task(self, task):
+ ''' Save the task in the backend. '''
+ tid = task.get_id()
+ if task not in self.to_set and tid not in self.to_remove:
+ self.to_set.appendleft(task)
+ self.__try_launch_setting_thread()
+
+ def __try_launch_setting_thread(self):
+ '''
+ Helper function to launch the setting thread, if it's not running
+ '''
+ if self.to_set_timer == None:
+ self.to_set_timer = threading.Timer(10.0, \
+ self.launch_setting_thread)
+ self.to_set_timer.start()
+
+ def launch_setting_thread(self):
+ while True:
+ try:
+ task = self.to_set.pop()
+ except IndexError:
+ break
+ #time.sleep(4)
+ tid = task.get_id()
+ if tid not in self.tids and tid not in self.to_remove:
+ self.tids.append(tid)
+ self.__save_task(task)
+ while True:
+ try:
+ tid = self.to_remove.pop()
+ except IndexError:
+ break
+ self.__remove_task(tid)
+ #we release the weak lock
+ self.to_set_timer = None
+
+ def __save_task(self, task):
+ tid = task.get_id()
+ existing = None
+ #First, we find the existing task from the treenode
+ for node in self.xmlproj.childNodes:
+ if node.getAttribute("id") == tid:
+ existing = node
+ t_xml = taskxml.task_to_xml(self.doc, task)
+ modified = False
+ #We then replace the existing node
+ if existing and t_xml:
+ #We will write only if the task has changed
+ if t_xml.toxml() != existing.toxml():
+ self.xmlproj.replaceChild(t_xml, existing)
+ modified = True
+ #If the node doesn't exist, we create it
+ # (it might not be the case in all backends
+ else:
+ self.xmlproj.appendChild(t_xml)
+ modified = True
+ #In this particular backend, we write all the tasks
+ #This is inherent to the XML file backend
+ if modified and self._parameters["path"] and self.doc :
+ cleanxml.savexml(self._parameters["path"], self.doc)
+
+ def remove_task(self, tid):
+ '''
+ Queues task to be removed.
+ @param tid: The Task ID of the task to be removed
+ '''
+ if tid not in self.to_remove:
+ self.to_remove.appendleft(tid)
+ self.__try_launch_setting_thread()
+ return None
+
+ def __remove_task(self, tid):
+ ''' Completely remove the task with ID = tid '''
+ for node in self.xmlproj.childNodes:
+ if node.getAttribute("id") == tid:
+ self.xmlproj.removeChild(node)
+ if tid in self.tids:
+ self.tids.remove(tid)
+ cleanxml.savexml(self._parameters["path"], self.doc)
+
+ def sync(self):
+ '''
+ Forces the backend to sync all the pending tasks
+ '''
+ if self.to_set_timer != None:
+ try:
+ self.to_set_timer.cancel()
+ self.to_set_timer.join(20)
+ except:
+ pass
+ self.launch_setting_thread()
+
+ def quit(self, disable = False):
+ '''
+ Called when GTG quits or disconnects the backend.
+ '''
+ super(Backend, self).quit(disable)
+ self.sync()
+ cleanxml.savexml(self._parameters["path"], self.doc, backup=True)
+
+ def get_number_of_tasks(self):
+ '''
+ Returns the number of tasks stored in the backend. Doesn't need to be a
+ fast function, is called just for the UI
+ '''
+ return len(self.tids)
=== added file 'GTG/backends/backendsignals.py'
--- GTG/backends/backendsignals.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/backendsignals.py 2010-05-31 15:39:30 +0000
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gobject
+
+from GTG.tools.borg import Borg
+
+
+
+class BackendSignals(Borg):
+ '''
+ This class handles the signals that involve backends.
+ In particular, it's a wrapper Borg class around a _BackendSignalsGObject
+ class, and all method of the wrapped class can be used as if they were part
+ of this class
+ '''
+
+
+ def __init__(self):
+ super(BackendSignals, self).__init__()
+ if not hasattr(self, "_gobject"):
+ self._gobject = BackendSignals._BackendSignalsGObject()
+
+ def __getattr__(self, attr):
+ return getattr(self._gobject, attr)
+
+ class _BackendSignalsGObject(gobject.GObject):
+
+ #signal name constants
+ BACKEND_STATE_TOGGLED = 'backend-state-toggled' #emitted when a
+ #backend is
+ #enabled or disabled
+ BACKEND_RENAMED = 'backend-renamed' #emitted when a backend is renamed
+ BACKEND_ADDED = 'backend-added'
+ BACKEND_REMOVED = 'backend-added' #when a backend is deleted
+
+ __string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
+ gobject.TYPE_NONE, (str, ))
+
+ __gsignals__ = {BACKEND_STATE_TOGGLED : __string_signal__, \
+ BACKEND_RENAMED : __string_signal__, \
+ BACKEND_ADDED : __string_signal__, \
+ BACKEND_REMOVED : __string_signal__}
+
+ ############# Signals #########
+ #connecting to signals is fine, but keep an eye if you should emit them.
+ #As a general rule, signals should only be emitted in the GenericBackend
+ #class
+
+ def _emit_signal(self, signal, backend_id):
+ gobject.idle_add(self.emit, signal, backend_id)
+
+ def backend_state_changed(self, backend_id):
+ self._emit_signal(self.BACKEND_STATE_TOGGLED, backend_id)
+
+ def backend_renamed(self, backend_id):
+ self._emit_signal(self.BACKEND_RENAMED, backend_id)
+
+ def backend_added(self, backend_id):
+ self._emit_signal(self.BACKEND_ADDED, backend_id)
+
+ def backend_removed(self, backend_id):
+ self._emit_signal(self.BACKEND_REMOVED, backend_id)
+
=== added file 'GTG/backends/genericbackend.py'
--- GTG/backends/genericbackend.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/genericbackend.py 2010-05-31 15:39:30 +0000
@@ -0,0 +1,372 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+'''
+FIXME: document!
+'''
+
+from GTG.backends.backendsignals import BackendSignals
+from GTG.tools.logger import Log
+
+
+
+class GenericBackend(object):
+ '''
+ Base class for every backend. It's a little more than an interface which
+ methods have to be redefined in order for the backend to run.
+ '''
+
+
+ #BACKEND TYPE DESCRIPTION
+ #"_general_description" is a dictionary that holds the values for the
+ # following keys:
+ BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
+ # translations, *must be unique*)
+ BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
+ BACKEND_DESCRIPTION = "description" #A short description of the backend
+ BACKEND_ICON_NAME = "icon-name" #name of the icon in the theme (filename)
+ BACKEND_AUTHORS = "authors" #a list of strings
+ BACKEND_TYPE = "type"
+ #BACKEND_TYPE is one of:
+ TYPE_READWRITE = "readwrite"
+ TYPE_READONLY = "readonly"
+ TYPE_IMPORT = "import"
+ TYPE_EXPORT = "export"
+ _general_description = {}
+
+
+ #"static_parameters" is a dictionary of dictionaries, each of which
+ #representing a parameter needed to configure the backend.
+ #each "sub-dictionary" is identified by this a key representing its name.
+ #"static_parameters" will be part of the definition of each
+ #particular backend.
+ # Each dictionary contains the keys:
+ #PARAM_DESCRIPTION = "description" #short description (shown to the user
+ # during configuration)
+ PARAM_DEFAULT_VALUE = "default_value" # its default value
+ PARAM_CONFIGURABLE = "configurable" # True if the user should
+ # see and change this
+ PARAM_TYPE = "type"
+ #PARAM_TYPE is one of the following (changing this changes the way
+ # the user can configure the parameter)
+ TYPE_FILENAME = "file" #a file chooser is added
+ TYPE_PASSWORD = "password" #the real password is stored in the GNOME
+ # keyring
+ # This is just a key to find it there
+ TYPE_STRING = "string" #generic string, nothing fancy is done
+ TYPE_INT = "int" #edit box can contain only integers
+ TYPE_BOOL = "bool" #checkbox is shown
+ _static_parameters = {}
+
+ def initialize(self):
+ '''
+ Called each time it is enabled again (including on backend creation).
+ Please note that a class instance for each disabled backend *is*
+ created, but it's not initialized.
+ Optional.
+ NOTE: make sure to call super().initialize()
+ '''
+ self._parameters[self.KEY_ENABLED] = True
+ #we signal that the backend has been enabled
+ self._signal_manager.backend_state_changed(self.get_id())
+
+ def start_get_tasks(self,push_task_func,task_factory_func):
+ '''
+ Once this function is launched, the backend can start pushing
+ tasks to gtg parameters.
+
+ @push_task_func: a function that takes a Task as parameter
+ and pushes it into GTG.
+ @task_factory_func: a function that takes a tid as parameter
+ and returns a Task object with the given pid.
+
+ @return: start_get_tasks() might not return or finish
+ '''
+ raise NotImplemented()
+
+ def set_task(self, task):
+ '''
+ Save the task in the backend. If the task id is new for the
+ backend, then a new task must be created
+ '''
+ raise NotImplemented()
+
+ def remove_task(self, tid):
+ ''' Completely remove the task with ID = tid '''
+ raise NotImplemented()
+
+ def has_task(self, tid):
+ '''Returns true if the backend has an internal idea
+ of the task corresponding to the tid. False otherwise'''
+ raise NotImplemented()
+
+ def new_task_id(self):
+ '''
+ Returns an available ID for a new task so that a task with this ID
+ can be saved with set_task later.
+ '''
+ raise NotImplemented()
+
+ def this_is_the_first_run(self, xml):
+ '''
+ Steps to execute if it's the first time the backend is run. Optional.
+ '''
+ pass
+
+ def purge(self):
+ '''
+ Called when a backend will be removed from GTG. Useful for removing
+ configuration files. Optional.
+ '''
+ pass
+
+ def get_number_of_tasks(self):
+ '''
+ Returns the number of tasks stored in the backend. Doesn't need to be a
+ fast function, is called just for the UI
+ '''
+ raise NotImplemented()
+
+ def quit(self, disable = False):
+ '''
+ Called when GTG quits or disconnects the backend. Remember to execute
+ also this function when quitting. If disable is True, the backend won't
+ be automatically loaded at next GTG start
+ '''
+ if disable:
+ self._parameters[self.KEY_ENABLED] = False
+ #we signal that we have been disabled
+ self._signal_manager.backend_state_changed(self.get_id())
+ #sync attached tags with the stored parameter
+ if self._attached_tags:
+ self._parameters[self.KEY_ATTACHED_TAGS] = \
+ reduce(lambda a, b: a + "," + b, self._attached_tags)
+ else:
+ self._parameters[self.KEY_ATTACHED_TAGS] = []
+
+###############################################################################
+###### You don't need to reimplement the functions below this line ############
+###############################################################################
+
+ #These parameters are common to all backends and necessary.
+ # They will be added automatically to your _static_parameters list
+ #NOTE: for now I'm disabling changing the default backend. Once it's all
+ # set up, we will see about that (invernizzi)
+ KEY_DEFAULT_BACKEND = "Default"
+ KEY_ENABLED = "Enabled"
+ KEY_HUMAN_NAME = BACKEND_HUMAN_NAME
+ KEY_ATTACHED_TAGS = "tags"
+ KEY_USER = "user"
+ ALLTASKS_TAG = "gtg-tags-all" #FIXME: moved here to avoid circular imports
+
+ _static_parameters_obligatory = { \
+ KEY_ATTACHED_TAGS: {\
+ PARAM_TYPE: TYPE_STRING, \
+ PARAM_DEFAULT_VALUE: ALLTASKS_TAG, \
+ PARAM_CONFIGURABLE: True, \
+ }, \
+ KEY_DEFAULT_BACKEND: { \
+ PARAM_TYPE: TYPE_BOOL, \
+ PARAM_DEFAULT_VALUE: False, \
+ PARAM_CONFIGURABLE: False, \
+ }, \
+ KEY_HUMAN_NAME: { \
+ PARAM_TYPE: TYPE_STRING, \
+ PARAM_DEFAULT_VALUE: "", \
+ PARAM_CONFIGURABLE: True, \
+ }, \
+ KEY_USER: { \
+ PARAM_TYPE: TYPE_STRING, \
+ PARAM_DEFAULT_VALUE: "", \
+ PARAM_CONFIGURABLE: True, \
+ }, \
+ KEY_ENABLED: { \
+ PARAM_TYPE: TYPE_BOOL, \
+ PARAM_DEFAULT_VALUE: False, \
+ PARAM_CONFIGURABLE: True, \
+ }}
+
+ #Handy dictionary used in type conversion (from string to type)
+ _type_converter = {TYPE_FILENAME: str,
+ TYPE_STRING: str,
+ TYPE_INT: int}
+
+ @classmethod
+ def _get_static_parameters(cls):
+ '''
+ Helper method, used to obtain the full list of the static_parameters
+ (user configured and default ones)
+ '''
+ if hasattr(cls, "_static_parameters"):
+ temp_dic = cls._static_parameters.copy()
+ for key, value in cls._static_parameters_obligatory.iteritems():
+ temp_dic[key] = value
+ return temp_dic
+ else:
+ raise NotImplemented("_static_parameters not implemented for " + \
+ "backend %s" % type(cls))
+
+ def __init__(self, parameters):
+ """
+ Instantiates a new backend. Please note that this is called also for
+ disabled backends. Those are not initialized, so you might want to check
+ out the initialize() function.
+ """
+ if self.KEY_DEFAULT_BACKEND not in parameters:
+ parameters[self.KEY_DEFAULT_BACKEND] = True
+ if parameters[self.KEY_DEFAULT_BACKEND] or \
+ not self.KEY_ATTACHED_TAGS in parameters:
+ self._attached_tags = [self.ALLTASKS_TAG]
+ else:
+ self._attached_tags = parameters[self.KEY_ATTACHED_TAGS].split(',')
+ self._parameters = parameters
+ self._signal_manager = BackendSignals()
+
+ def get_attached_tags(self):
+ '''
+ Returns the list of tags which are handled by this backend
+ '''
+ if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
+ self._parameters[self.KEY_DEFAULT_BACKEND]:
+ return [self.ALLTASKS_TAG]
+ return self._attached_tags
+
+ def set_attached_tags(self, tags):
+ '''
+ Changes the set of attached tags
+ '''
+ self._attached_tags = tags
+
+ @classmethod
+ def get_static_parameters(cls):
+ """
+ Returns a dictionary of parameters necessary to create a backend.
+ """
+ return cls._get_static_parameters()
+
+ def get_parameters(self):
+ """
+ Returns a dictionary of the current parameters.
+ """
+ return self._parameters
+
+ @classmethod
+ def get_name(cls):
+ """
+ Returns the name of the backend as it should be displayed in the UI
+ """
+ return cls._get_from_general_description(cls.BACKEND_NAME)
+
+ @classmethod
+ def get_description(cls):
+ """Returns a description of the backend"""
+ return cls._get_from_general_description(cls.BACKEND_DESCRIPTION)
+
+ @classmethod
+ def get_type(cls):
+ """Returns the backend type(readonly, r/w, import, export) """
+ return cls._get_from_general_description(cls.BACKEND_TYPE)
+
+ @classmethod
+ def get_authors(cls):
+ '''
+ returns the backend author(s)
+ '''
+ return cls._get_from_general_description(cls.BACKEND_AUTHORS)
+
+ @classmethod
+ def _get_from_general_description(cls, key):
+ '''
+ Helper method to extract values from cls._general_description.
+ Raises an exception if the key is missing (helpful for developers
+ adding new backends).
+ '''
+ if key in cls._general_description:
+ return cls._general_description[key]
+ else:
+ raise NotImplemented("Key %s is missing from " +\
+ "'self._general_description' of a backend (%s). " +
+ "Please add the corresponding value" % (key, type(cls)))
+
+ @classmethod
+ def cast_param_type_from_string(cls, param_value, param_type):
+ '''
+ Parameters are saved in a text format, so we have to cast them to the
+ appropriate type on loading. This function does exactly that.
+ '''
+ if param_type in cls._type_converter:
+ return cls._type_converter[param_type](param_value)
+ elif param_type == cls.TYPE_BOOL:
+ if param_value == "True":
+ return True
+ elif param_value == "False":
+ return False
+ else:
+ raise Exception("Unrecognized bool value '%s'" %
+ param_type)
+ else:
+ raise NotImplemented("I don't know what type is '%s'" %
+ param_type)
+
+ def get_id(self):
+ '''
+ returns the backends id, used in the datastore for indexing backends
+ '''
+ return self.get_name() + "@" + self._parameters["pid"]
+
+ @classmethod
+ def get_human_default_name(cls):
+ '''
+ returns the user friendly default backend name.
+ '''
+ return cls._general_description[cls.BACKEND_HUMAN_NAME]
+
+ def get_human_name(self):
+ '''
+ returns the user customized backend name. If the user hasn't
+ customized it, returns the default one
+ '''
+ if self.KEY_HUMAN_NAME in self._parameters and \
+ self._parameters[self.KEY_HUMAN_NAME] != "":
+ return self._parameters[self.KEY_HUMAN_NAME]
+ else:
+ return self.get_human_default_name()
+
+ def set_human_name(self, name):
+ '''
+ sets a custom name for the backend
+ '''
+ static_parameters = self.get_static_parameters()[self.KEY_HUMAN_NAME]
+ if static_parameters[self.PARAM_CONFIGURABLE]:
+ self._parameters[self.KEY_HUMAN_NAME] = name
+ #we signal the change
+ self._signal_manager.backend_renamed(self.get_id())
+
+ def is_enabled(self):
+ '''
+ Returns if the backend is enabled
+ '''
+ return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
+ self.is_default()
+
+ def is_default(self):
+ '''
+ Returns if the backend is enabled
+ '''
+ return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
=== removed file 'GTG/backends/localfile.py'
--- GTG/backends/localfile.py 2010-04-26 23:12:57 +0000
+++ GTG/backends/localfile.py 1970-01-01 00:00:00 +0000
@@ -1,176 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Gettings Things Gnome! - a personal organizer for the GNOME desktop
-# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
-#
-# 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/>.
-# -----------------------------------------------------------------------------
-
-'''
-Localfile is a read/write backend that will store your tasks in an XML file
-This file will be in your $XDG_DATA_DIR/gtg folder.
-'''
-
-import os
-import uuid
-
-from GTG.core import CoreConfig
-from GTG.tools import cleanxml, taskxml
-
-def get_name():
- """Returns the name of the backend as it should be displayed in the UI"""
- return "Local File"
-
-def get_description():
- """Returns a description of the backend"""
- return "Your tasks are saved in an XML file located in your HOME folder"
-
-def get_parameters():
- """
- Returns a dictionary of parameters. Keys should be strings and
- are the name of the parameter.
- Values are string with value : string, password, int, bool
- and are an information about the type of the parameter
- Currently, only string is supported.
- """
- dic = {}
- dic["filename"] = "string"
- return dic
-
-def get_features():
- """Returns a dict of features supported by this backend"""
- return {}
-
-def get_type():
- """Type is one of : readwrite, readonly, import, export"""
- return "readwrite"
-
-class Backend:
- def __init__(self, parameters, firstrunxml=None):
- """
- Instantiates a new backend.
-
- @param parameters: should match the dictionary returned in
- get_parameters. Anyway, the backend should care if one expected value is
- None or does not exist in the dictionary.
- @firstrun: only needed for the default backend. It should be omitted for
- all other backends.
- """
- self.tids = []
- self.pid = 1
- if "filename" in parameters:
- zefile = parameters["filename"]
- #If zefile is None, we create a new file
- else:
- zefile = "%s.xml" %(uuid.uuid4())
- parameters["filename"] = zefile
- #For the day we want to open files somewhere else
- default_folder = True
- if default_folder:
- self.zefile = os.path.join(CoreConfig.DATA_DIR, zefile)
- self.filename = zefile
- else:
- self.zefile = zefile
- self.filename = zefile
- #Create the default tasks for the first run.
- #We write the XML object in a file
- if firstrunxml and not os.path.exists(zefile):
- #shutil.copy(firstrunfile,self.zefile)
- cleanxml.savexml(self.zefile, firstrunxml)
- self.doc, self.xmlproj = cleanxml.openxmlfile(self.zefile, "project")
-
- def start_get_tasks(self,push_task_func,task_factory_func):
- '''
- Once this function is launched, the backend can start pushing
- tasks to gtg parameters.
-
- @push_task_func: a function that takes a Task as parameter
- and pushes it into GTG.
- @task_factory_func: a function that takes a tid as parameter
- and returns a Task object with the given pid.
-
- @return: start_get_tasks() might not return or finish
- '''
- tid_list = []
- for node in self.xmlproj.childNodes:
- #time.sleep(2)
- tid = node.getAttribute("id")
- if tid not in self.tids:
- self.tids.append(tid)
- task = task_factory_func(tid)
- task = taskxml.task_from_xml(task,node)
- push_task_func(task)
- #print "#### finishing pushing tasks"
-
- def set_task(self, task):
- ''' Save the task in the backend '''
- #time.sleep(4)
- tid = task.get_id()
- if tid not in self.tids:
- self.tids.append(tid)
- existing = None
- #First, we find the existing task from the treenode
- for node in self.xmlproj.childNodes:
- if node.getAttribute("id") == tid:
- existing = node
- t_xml = taskxml.task_to_xml(self.doc, task)
- modified = False
- #We then replace the existing node
- if existing and t_xml:
- #We will write only if the task has changed
- if t_xml.toxml() != existing.toxml():
- self.xmlproj.replaceChild(t_xml, existing)
- modified = True
- #If the node doesn't exist, we create it
- # (it might not be the case in all backends
- else:
- self.xmlproj.appendChild(t_xml)
- modified = True
- #In this particular backend, we write all the tasks
- #This is inherent to the XML file backend
- if modified and self.zefile and self.doc :
- cleanxml.savexml(self.zefile, self.doc)
- return None
-
- def remove_task(self, tid):
- ''' Completely remove the task with ID = tid '''
- for node in self.xmlproj.childNodes:
- if node.getAttribute("id") == tid:
- self.xmlproj.removeChild(node)
- if tid in self.tids:
- self.tids.remove(tid)
- cleanxml.savexml(self.zefile, self.doc)
-
- def new_task_id(self):
- '''
- Returns an available ID for a new task so that a task with this ID
- can be saved with set_task later.
- If None, then GTG will create a new ID by itself.
- The ID cannot contain the character "@".
- '''
- k = 0
- pid = self.pid
- newid = "%s@%s" %(k, pid)
- while str(newid) in self.tids:
- k += 1
- newid = "%s@%s" %(k, pid)
- self.tids.append(newid)
- return newid
-
- def quit(self):
- '''
- Called when GTG quits or disconnects the backend.
- (Subclasses might pass here)
- '''
- cleanxml.savexml(self.zefile, self.doc, backup=True)
=== modified file 'GTG/core/__init__.py'
--- GTG/core/__init__.py 2010-05-26 09:54:42 +0000
+++ GTG/core/__init__.py 2010-05-31 15:39:30 +0000
@@ -40,11 +40,15 @@
#=== IMPORT ====================================================================
import os
-from xdg.BaseDirectory import xdg_data_home, xdg_config_home
-from GTG.tools import cleanxml
-from configobj import ConfigObj
-
-from GTG.core import firstrun_tasks
+from xdg.BaseDirectory import xdg_data_home, xdg_config_home
+from configobj import ConfigObj
+
+import GTG
+from GTG.core import firstrun_tasks
+from GTG.tools import cleanxml
+from GTG.backends import BackendFactory
+from GTG.tools.logger import Log
+
class CoreConfig:
@@ -60,6 +64,8 @@
#DBUS
BUSNAME = "org.GTG"
BUSINTERFACE = "/org/GTG"
+ #TAGS
+ ALLTASKS_TAG = "gtg-tags-all"
def __init__(self):
if not os.path.exists(self.CONF_DIR):
@@ -81,96 +87,53 @@
self.conf_dict = ConfigObj(self.CONF_DIR + self.CONF_FILE)
self.task_conf_dict = ConfigObj(self.CONF_DIR + self.TASK_CONF_FILE)
- def save_config(self):
+ def save(self):
+ ''' Saves the configuration of CoreConfig '''
self.conf_dict.write()
self.task_conf_dict.write()
-
+
+ @staticmethod
+ def get_icons_directories():
+ '''
+ Returns the directories containing the icons
+ '''
+ return [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]
+
def get_backends_list(self):
- backend_fn = []
-
- # Check if config dir exists, if not create it
- if not os.path.exists(self.DATA_DIR):
- os.makedirs(self.DATA_DIR)
-
+ backends_dic = self._read_backend_configuration_file()
+
+ #Retrocompatibility: default backend has changed name
+ for dic in backends_dic:
+ if dic["module"] == "localfile":
+ dic["module"] = "backend_localfile"
+ dic["need_conversion"] = \
+ dic["xmlobject"].getAttribute("filename")
+
+ #Now that the backend list is build, we will construct them
+ for dic in backends_dic:
+ BackendFactory().restore_backend_from_xml(dic)
+ #If no backend available, we create a new using localfile. Xmlobject
+ # will be filled in by the backend
+ if len(backends_dic) == 0:
+ dic = BackendFactory().get_new_backend_dict( \
+ "backend_localfile")
+ dic["backend"].this_is_the_first_run(firstrun_tasks.populate())
+ backends_dic.append(dic)
+ return backends_dic
+
+ def _read_backend_configuration_file(self):
+ '''
+ Reads the file describing the current backend configuration (project.xml)
+ and returns a list of dictionaries, each containing:
+ - the xml object defining the backend characteristics under
+ "xmlobject"
+ - the name of the backend under "module"
+ '''
# Read configuration file, if it does not exist, create one
- datafile = self.DATA_DIR + self.DATA_FILE
- doc, configxml = cleanxml.openxmlfile(datafile,"config") #pylint: disable-msg=W0612
+ datafile = os.path.join(self.DATA_DIR, self.DATA_FILE)
+ doc, configxml = cleanxml.openxmlfile(datafile, "config")
xmlproject = doc.getElementsByTagName("backend")
- # collect configred backends
- pid = 1
- for xp in xmlproject:
- dic = {}
- #We have some retrocompatibility code
- #A backend without the module attribute is pre-rev.105
- #and is considered as "filename"
- if xp.hasAttribute("module"):
- dic["module"] = str(xp.getAttribute("module"))
- dic["pid"] = str(xp.getAttribute("pid"))
- #The following "else" could be removed later
- else:
- dic["module"] = "localfile"
- dic["pid"] = str(pid)
-
- dic["xmlobject"] = xp
- pid += 1
- backend_fn.append(dic)
-
- firstrun = False
- #If no backend available, we create a new using localfile
- if len(backend_fn) == 0:
- dic = {}
- dic["module"] = "localfile"
- dic["pid"] = "1"
- backend_fn.append(dic)
- firstrun = True
-
- #Now that the backend list is build, we will construct them
- #Remember that b is a dictionnary
- for b in backend_fn:
- #We dynamically import modules needed
- module_name = "GTG.backends.%s"%b["module"]
- #FIXME : we should throw an error if the backend is not importable
- module = __import__(module_name)
- module = getattr(module, "backends")
- classobj = getattr(module, b["module"])
- b["parameters"] = classobj.get_parameters()
- #If creating the default backend, we don't have the xmlobject yet
- if "xmlobject" in b:
- xp = b.pop("xmlobject")
- #We will try to get the parameters
- for key in b["parameters"]:
- if xp.hasAttribute(key):
- b[key] = str(xp.getAttribute(key))
- if firstrun:
- frx = firstrun_tasks.populate()
- back = classobj.Backend(b,firstrunxml=frx)
- else:
- back = classobj.Backend(b)
- #We put the backend itself in the dic
- b["backend"] = back
-
- return backend_fn
-
-
- #If initial save, we don't close stuffs.
- def save_datastore(self,ds,initial_save=False):
- doc,xmlconfig = cleanxml.emptydoc("config")
- for b in ds.get_all_backends():
- param = b.get_parameters()
- t_xml = doc.createElement("backend")
- for key in param:
- #We dont want parameters,backend,xmlobject
- if key not in ["backend","parameters","xmlobject"]:
- t_xml.setAttribute(str(key),str(param[key]))
- #Saving all the projects at close
- xmlconfig.appendChild(t_xml)
- if not initial_save:
- b.quit()
-
- datafile = self.DATA_DIR + self.DATA_FILE
- cleanxml.savexml(datafile,doc,backup=True)
-
- #Saving the tagstore
- if not initial_save:
- ts = ds.get_tagstore()
- ts.save()
+ # collect configured backends
+ return [{"xmlobject": xp, \
+ "module": xp.getAttribute("module")} for xp in xmlproject]
+
=== modified file 'GTG/core/datastore.py'
--- GTG/core/datastore.py 2010-05-26 08:55:45 +0000
+++ GTG/core/datastore.py 2010-05-31 15:39:30 +0000
@@ -18,267 +18,495 @@
# -----------------------------------------------------------------------------
"""
-datastore contains a list of TagSource objects, which are proxies between a backend and the datastore itself
+The DaataStore contains a list of TagSource objects, which are proxies
+between a backend and the datastore itself
"""
import threading
-import gobject
-import time
-
-from GTG.core import tagstore, requester
-from GTG.core.task import Task
-from GTG.core.tree import Tree
-
-
-#Only the datastore should access to the backend
-DEFAULT_BACKEND = "1"
-#If you want to debug a backend, it can be useful to disable the threads
-#Currently, it's python threads (and not idle_add, which is not useful)
-THREADING = True
-
-
-class DataStore:
- """ A wrapper around a backend that provides an API for adding/removing tasks """
+import uuid
+import os.path
+from collections import deque
+
+from GTG.core import tagstore, requester
+from GTG.core.task import Task
+from GTG.core.tree import Tree
+from GTG.core import CoreConfig
+from GTG.tools.logger import Log
+from GTG.backends.genericbackend import GenericBackend
+from GTG.tools import cleanxml
+from GTG.backends.backendsignals import BackendSignals
+
+
+class DataStore(object):
+ '''
+ A wrapper around all backends that is responsible for keeping the backend
+ instances. It can enable, disable, register and destroy backends, and acts
+ as interface between the backends and GTG core.
+ You should not interface yourself directly with the DataStore: use the
+ Requester instead (which also sends signals as you issue commands).
+ '''
+
+
def __init__(self):
- """ Initializes a DataStore object """
- self.backends = {}
+ '''
+ Initializes a DataStore object
+ '''
+ self.backends = {} #dictionary {backend_name_string: Backend instance}
self.open_tasks = Tree()
-# self.closed_tasks = Tree()
self.requester = requester.Requester(self)
self.tagstore = tagstore.TagStore(self.requester)
-
- def all_tasks(self):
- """
+ self._backend_signals = BackendSignals()
+ self.mutex = threading.RLock()
+
+ ##########################################################################
+ ### Helper functions (get_ methods for Datastore embedded objects)
+ ##########################################################################
+
+ def get_tagstore(self):
+ '''
+ Helper function to obtain the Tagstore associated with this DataStore
+ @return GTG.core.tagstore.TagStore: the tagstore object
+ '''
+ return self.tagstore
+
+ def get_requester(self):
+ '''
+ Helper function to get the Requester associate with this DataStore
+ @returns GTG.core.requester.Requester: the requester associated with
+ this datastore
+ '''
+ return self.requester
+
+ def get_tasks_tree(self):
+ '''
+ Helper function to get a Tree with all the tasks contained in this
+ Datastore
+ @returns GTG.core.tree.Tree: a task tree (the main one)
+ '''
+ return self.open_tasks
+
+ ##########################################################################
+ ### Tasks functions
+ ##########################################################################
+
+ def get_all_tasks(self):
+ '''
Returns list of all keys of open tasks
- """
+ @return a list of strings: a list of task ids
+ '''
return self.open_tasks.get_all_keys()
def has_task(self, tid):
- """
+ '''
Returns true if the tid is among the open or closed tasks for
this DataStore, False otherwise.
- param tid: Task ID to search for
- """
- return self.open_tasks.has_node(tid) #or self.closed_tasks.has_node(tid)
+ @param tid: Task ID to search for
+ @return bool: True if the task is present
+ '''
+ return self.open_tasks.has_node(tid)
def get_task(self, tid):
- """
+ '''
Returns the internal task object for the given tid, or None if the
tid is not present in this DataStore.
@param tid: Task ID to retrieve
- """
- if tid:
- if self.has_task(tid):
- task = self.__internal_get_task(tid)
- else:
- #print "no task %s" %tid
- task = None
- return task
+ @returns GTG.core.task.Task or None: whether the Task is present
+ or not
+ '''
+ if self.has_task(tid):
+ return self.open_tasks.get_node(tid)
else:
- print "get_task should take a tid"
+ Log.debug("requested non-existent task")
return None
- def __internal_get_task(self, tid):
- return self.open_tasks.get_node(tid)
-# if toreturn == None:
-# self.closed_tasks.get_node(tid)
- #else:
- #print "error : this task doesn't exist in either tree"
- #pass
- #we return None if the task doesn't exist
-# return toreturn
-
- def delete_task(self, tid):
- """
- Deletes the given task entirely from this DataStore, and unlinks
- it from the task's parent.
- @return: True if task was deleted, or False if the tid was not
- present in this DataStore.
- """
- if not tid or not self.has_task(tid):
- return False
-
- self.__internal_get_task(tid).delete()
- uid, pid = tid.split('@') #pylint: disable-msg=W0612
- back = self.backends[pid]
- #Check that the task still exist. It might have been deleted
- #by its parent a few line earlier :
- if self.has_task(tid):
- self.open_tasks.remove_node(tid)
-# self.closed_tasks.remove_node(tid)
- back.remove_task(tid)
- return True
-
- def new_task(self,pid=None):
+ def task_factory(self, tid, newtask = False):
+ '''
+ Instantiates the given task id as a Task object.
+ @param tid: a task id. Must be unique
+ @param newtask: True if the task has never been seen before
+ @return Task: a Task instance
+ '''
+ return Task(tid, self.requester, newtask)
+
+ def new_task(self):
"""
Creates a blank new task in this DataStore.
- @param pid: (Optional) parent ID that this task should be a child of.
- If not specified, the task will be a child of the default backend.
+ New task is created in all the backends that collect all tasks (among
+ them, the default backend). The default backend uses the same task id
+ in its own internal representation.
@return: The task object that was created.
"""
- if not pid:
- pid = DEFAULT_BACKEND
- newtid = self.backends[pid].new_task_id()
- while self.has_task(newtid):
- print "error : tid already exists"
- newtid = self.backends[pid].new_task_id()
- task = Task(newtid, self.requester,newtask=True)
+ task = self.task_factory(uuid.uuid4(), True)
self.open_tasks.add_node(task)
- task.set_sync_func(self.backends[pid].set_task,callsync=False)
return task
-
- def get_tagstore(self):
- return self.tagstore
-
- def get_requester(self):
- return self.requester
-
- def get_tasks_tree(self):
- """ return: Open tasks tree """
- return self.open_tasks
-
- def push_task(self,task):
- """
- Adds the given task object as a node to the open tasks tree.
- @param task: A valid task object
- """
- tid = task.get_id()
- if self.has_task(tid):
- print "pushing an existing task. We should care about modifications"
+
+ def push_task(self, task, backend_capabilities = 'bypass for now'):
+ '''
+ Adds the given task object to the task tree. In other words, registers
+ the given task in the GTG task set.
+ @param task: A valid task object (a GTG.core.task.Task)
+ @return bool: True if the task has been accepted
+ '''
+ # self.mutex.acquire()
+ if self.has_task(task.get_id()):
+ return False
+ #Work in progress here
+ loaded_task = self.get_task(task.get_id())
+ if loaded_task.get_modified() < task.get_modified:
+ print '''check backend capabilities and set the corresponding
+ attributes, including the modified_time.
+ '''
+ #self.mutex.release()
+ return False
else:
- uid, pid = tid.split('@')
self.open_tasks.add_node(task)
task.set_loaded()
- task.set_sync_func(self.backends[pid].set_task,callsync=False)
-
- def task_factory(self,tid):
- """
- Instantiates the given task id as a Task object.
- @param tid: The id of the task to instantiate
- @return: The task object instantiated for tid
- """
- task = None
- if self.has_task(tid):
- print "error : tid already exists"
+ #self.mutex.release()
+ return True
+
+ ##########################################################################
+ ### Backends functions
+ ##########################################################################
+
+ def get_all_backends(self, disabled = False):
+ """
+ returns list of all registered backends for this DataStore.
+ @param disabled: If disabled is True, attaches also the list of disabled backends
+ @return list: a list of TaskSource objects
+ """
+ #NOTE: consider cashing this result for speed.
+ result = []
+ for backend in self.backends.itervalues():
+ if backend.is_enabled() or disabled:
+ result.append(backend)
+ return result
+
+ def get_backend(self, backend_id):
+ '''
+ Returns a backend given its id
+ @param backend_id: a backend id
+ @returns GTG.core.datastore.TaskSource or None: the requested backend,
+ or none
+ '''
+ if backend_id in self.backends:
+ return self.backends[backend_id]
else:
- task = Task(tid, self.requester, newtask=False)
- return task
-
+ return None
- def register_backend(self, dic):
+ def register_backend(self, backend_dic):
"""
Registers a TaskSource as a backend for this DataStore
- @param dic: Dictionary object with a "backend" and "pid"
- specified. dic["pid"] should be the parent ID to use
- with the backend specified in dic["backend"].
+ @param backend_dic: Dictionary object containing all the
+ parameters to initialize the backend (filename...). It should
+ also contain the backend class (under "backend"), and its unique
+ id (under "pid")
"""
- if "backend" in dic:
- pid = dic["pid"]
- backend = dic["backend"]
- source = TaskSource(backend, dic)
- self.backends[pid] = source
- #Filling the backend
- #Doing this at start is more efficient than
- #after the GUI is launched
- source.start_get_tasks(self.push_task,self.task_factory)
+ if "backend" in backend_dic:
+ if "pid" not in backend_dic:
+ Log.debug("registering a backend without pid.")
+ return
+ backend = backend_dic["backend"]
+ #Checking that is a new backend
+ if backend.get_id() in self.backends:
+ Log.debug("registering already registered backend")
+ return
+ source = TaskSource(requester = self.requester,
+ backend = backend,
+ task_factory = self.task_factory,
+ push_task = self.push_task,
+ get_task = self.get_task)
+ self.backends[backend.get_id()] = source
+ #we notify that a new backend is present
+ self._backend_signals.backend_added(backend.get_id())
+ #saving the backend in the correct dictionary (backends for enabled
+ # backends, disabled_backends for the disabled ones)
+ if not GenericBackend.KEY_ENABLED in backend_dic:
+ #this is useful for retro-compatibility
+ backend_dic[GenericBackend.KEY_ENABLED] = True
+ #if it's enabled, we initialize it
+ if source.is_enabled():
+ source.initialize(connect_signals = False)
+ #Filling the backend
+ #Doing this at start is more efficient than
+ #after the GUI is launched
+ source.start_get_tasks()
else:
- print "Register a dic without backend key: BUG"
-
- def unregister_backend(self, backend):
- """ Unimplemented """
- print "unregister backend %s not implemented" %backend
-
- def get_all_backends(self):
- """ returns list of all registered backends for this DataStore """
- l = []
- for key in self.backends:
- l.append(self.backends[key])
- return l
+ Log.debug("Tried to register a backend without a pid")
+
+ def set_backend_enabled(self, backend_id, state):
+ """
+ The backend corresponding to backend_id is enabled or disabled
+ according to "state".
+ Disable:
+ Quits a backend and disables it (which means it won't be
+ automatically loaded next time GTG is started)
+ Enable:
+ Reloads a disabled backend. Backend must be already known by the
+ Datastore
+ @parma backend_id: a backend id
+ @param state: True to enable, False to disable
+ """
+ if backend_id in self.backends:
+ backend = self.backends[backend_id]
+ current_state = backend.is_enabled()
+ if current_state == True and state == False:
+ #we disable the backend
+ backend.quit(disable = True)
+ elif current_state == False and state == True:
+ backend.initialize()
+ self.flush_all_tasks(backend_id)
+
+ def remove_backend(self, backend_id):
+ '''
+ Removes a backend, and forgets it ever existed.
+ @param backend_id: a backend id
+ '''
+ if backend_id in self.backends:
+ backend = self.backends[backend_id]
+ if backend.is_enabled():
+ self.set_backend_enabled(backend_id, False)
+ backend.purge()
+ #we notify that the backend has been deleted
+ self._backend_signals.backend_removed(backend.get_id())
+ del self.backends[backend_id]
+
+ def backend_change_attached_tags(self, backend_id, tag_names):
+ '''
+ Changes the tags for which a backend should store a task
+ @param backend_id: a backend_id
+ @param tag_names: the new set of tags. This should not be a tag object,
+ just the tag name.
+ '''
+ backend = self.backends[backend_id]
+ backend.set_attached_tags(tag_names)
+
+ def flush_all_tasks(self, backend_id):
+ '''
+ This function will cause all tasks to be checked against the backend
+ identified with backend_id. If tasks need to be added or removed, it
+ will be done here.
+ It has to be run after the creation of a new backend (or an alteration
+ of its "attached tags"), so that the tasks which are already loaded in
+ the Tree will be saved in the proper backends
+ @param backend_id: a backend id
+ '''
+ def _internal_flush_all_tasks():
+ backend = self.backends[backend_id]
+ for task_id in self.requester.get_all_tasks_list():
+ if backend.should_task_id_be_stored(task_id):
+ backend.set_task(None, task_id)
+ else:
+ backend.remove_task(None, task_id)
+ t = threading.Thread(target = _internal_flush_all_tasks).start()
+ self.backends[backend_id].start_get_tasks()
+
+ def save(self, quit = False):
+ '''
+ Saves the backends parameters.
+ @param quit: If quit is true, backends are shut down
+ '''
+ doc,xmlconfig = cleanxml.emptydoc("config")
+ #we ask all the backends to quit first.
+ if quit:
+ for b in self.get_all_backends():
+ #NOTE:we could do this in parallel. Maybe a quit and
+ #has_quit would be faster (invernizzi)
+ b.quit()
+ #we save the parameters
+ for b in self.get_all_backends(disabled = True):
+ param = b.get_parameters()
+ t_xml = doc.createElement("backend")
+ for key in param:
+ #We dont want parameters,backend,xmlobject
+ if key not in ["backend", "xmlobject"]:
+ t_xml.setAttribute(str(key),str(param[key]))
+ #Saving all the projects at close
+ xmlconfig.appendChild(t_xml)
+
+ datafile = os.path.join(CoreConfig.DATA_DIR, CoreConfig.DATA_FILE)
+ cleanxml.savexml(datafile,doc,backup=True)
+
+ #Saving the tagstore
+ ts = self.get_tagstore()
+ ts.save()
class TaskSource():
- """ transparent interface between the real backend and the datastore """
- def __init__(self, backend, parameters):
+ '''
+ Transparent interface between the real backend and the DataStore.
+ Is in charge of connecting and disconnecting to signals
+ '''
+ def __init__(self, requester, backend, task_factory, push_task, get_task):
"""
Instantiates a TaskSource object.
- @param backend: (Required) Task Backend being wrapperized
- @param parameters: Dictionary of custom parameters.
+ @param backend: the backend being wrapped
+ @param task_factory: a function capable of generating tasks
+ @param push_task: a function to inject tasks into GTG
+ @param get_task: a function to obtain a Task object from a tid
"""
self.backend = backend
- self.dic = parameters
- self.to_set = []
- self.to_remove = []
- self.lock = threading.Lock()
- self.count_set = 0
+ self.req = requester
+ self.task_factory = task_factory
+ self.push_task = push_task
+ self.get_task = get_task
+ self.to_set = deque()
+ self.to_remove = deque()
+ self.task_filter = self.get_task_filter_for_backend()
+ self.set_task_handle = None
+ self.remove_task_handle = None
+ self.to_set_timer = None
- def start_get_tasks(self,push_task,task_factory):
- """
+ def start_get_tasks(self):
+ ''''
Maps the TaskSource to the backend and starts threading.
- This must be called before the DataStore is usable.
- """
+ '''
func = self.backend.start_get_tasks
- t = threading.Thread(target=func,args=(push_task,task_factory))
+ t = threading.Thread(target=func,args=(self.push_task,\
+ self.task_factory))
t.start()
-
- def set_task(self, task):
+ #FIXME: this should be postponed
+ self._connect_signals()
+
+ def get_task_filter_for_backend(self):
+ '''
+ Fiter that checks if the task should be stored in this backend.
+
+ @returns function: a function that accepts a task and returns True/False
+ whether the task should be stored or not
+ '''
+ raw_filter = self.req.get_filter_bank().tags_intersection
+ return lambda task: raw_filter(task, \
+ set(self.backend.get_attached_tags()))
+
+ def should_task_id_be_stored(self, task_id):
+ '''
+ Helper function: Checks if a task should be stored in this backend
+ @param task_id: a task id
+ @returns bool: True if the task should be stored
+ '''
+ task = self.req.get_task(task_id)
+ return self.task_filter(task)
+
+ def set_task(self, sender, tid):
"""
Updates the task in the DataStore. Actually, it adds the task to a
queue to be updated asynchronously.
+ @param sender: not used, any value will do.
@param task: The Task object to be updated.
"""
- tid = task.get_id()
- if task not in self.to_set and tid not in self.to_remove:
- self.to_set.append(task)
- if self.lock.acquire(False):
- func = self.setting_thread
- t = threading.Thread(target=func)
- t.start()
-# else:
-# print "cannot acquire lock : not a problem, just for debug purpose"
+ if self.should_task_id_be_stored(tid):
+ if tid not in self.to_set and tid not in self.to_remove:
+ self.to_set.appendleft(tid)
+ self.__try_launch_setting_thread()
+ else:
+ self.remove_task(None, tid)
- def setting_thread(self):
- """
+ def launch_setting_thread(self):
+ '''
Operates the threads to set and remove tasks.
Releases the lock when it is done.
- """
- try:
- while len(self.to_set) > 0:
- t = self.to_set.pop(0)
- tid = t.get_id()
- if tid not in self.to_remove:
- self.count_set += 1
- #print "saving task %s (%s saves)" %(tid,self.count_set)
- self.backend.set_task(t)
- while len(self.to_remove) > 0:
- tid = self.to_remove.pop(0)
- self.backend.remove_task(tid)
- finally:
- self.lock.release()
+ '''
+ #FIXME: the lock should be general for all backends. Therefore, it
+ #should be handled in the datastore
+ while True:
+ try:
+ tid = self.to_set.pop()
+ except IndexError:
+ break
+ #we check that the task is not already marked for deletion
+ #and that it's still to be stored in this backend
+ #NOTE: no need to lock, we're reading
+ if tid not in self.to_remove and \
+ self.should_task_id_be_stored(tid) and \
+ self.req.has_task(tid):
+ task = self.req.get_task(tid)
+ self.backend.set_task(task)
+ while True:
+ try:
+ tid = self.to_remove.pop()
+ except IndexError:
+ break
+ self.backend.remove_task(tid)
+ #we release the weak lock
+ self.to_set_timer = None
- def remove_task(self, tid):
- """
+ def remove_task(self, sender, tid):
+ '''
Queues task to be removed.
+ @param sender: not used, any value will do
@param tid: The Task ID of the task to be removed
- """
+ '''
if tid not in self.to_remove:
- self.to_remove.append(tid)
- if self.lock.acquire(False):
- func = self.setting_thread
- t = threading.Thread(target=func)
- t.start()
-
- def new_task_id(self):
- """
- returns a new ID created by the backend.
- """
- return self.backend.new_task_id()
-
- def quit(self):
- """ Quits the backend """
- self.backend.quit()
-
- #Those functions are only for TaskSource
- def get_parameters(self):
- """
- Returns the parameters specified during creation of the DataStore
- """
- return self.dic
+ self.to_remove.appendleft(tid)
+ self.__try_launch_setting_thread()
+
+ def __try_launch_setting_thread(self):
+ '''
+ Helper function to launch the setting thread, if it's not running
+ '''
+ if self.to_set_timer == None:
+ self.to_set_timer = threading.Timer(10.0, \
+ self.launch_setting_thread)
+ self.to_set_timer.start()
+
+ def initialize(self, connect_signals = True):
+ '''
+ Initializes the backend and starts looking for signals.
+ @param connect_signals: if True, it starts listening for signals
+ '''
+ self.backend.initialize()
+ if connect_signals:
+ self._connect_signals()
+
+ def _connect_signals(self):
+ '''
+ Helper function to connect signals
+ '''
+ if not self.set_task_handle:
+ self.set_task_handle = self.req.connect('task-to-be-synced', \
+ self.set_task)
+ if not self.remove_task_handle:
+ self.remove_task_handle = self.req.connect('task-deleted',\
+ self.remove_task)
+
+ def _disconnect_signals(self):
+ '''
+ Helper function to disconnect signals
+ '''
+ if self.set_task_handle:
+ self.req.disconnect(self.set_task_handle)
+ self.set_task_handle = None
+ if self.remove_task_handle:
+ self.req.disconnect(self.remove_task_handle)
+ self.remove_task_handle = None
+
+ def sync(self):
+ '''
+ Forces the TaskSource to sync all the pending tasks
+ '''
+ if self.to_set_timer != None:
+ try:
+ self.to_set_timer.cancel()
+ self.to_set_timer.join(20)
+ except:
+ pass
+ self.launch_setting_thread()
+
+ def quit(self, disable = False):
+ '''
+ Quits the backend and disconnect the signals
+ @param disable: if True, the backend is disabled.
+ '''
+ self._disconnect_signals()
+ self.sync()
+ self.backend.quit(disable)
+
+ def __getattr__(self, attr):
+ '''
+ Delegates all the functions not defined here to the real backend
+ (standard python function)
+ @param attr: attribute to get
+ '''
+ if attr in self.__dict__:
+ return self.__dict__[attr]
+ else:
+ return getattr(self.backend, attr)
=== modified file 'GTG/core/filteredtree.py'
--- GTG/core/filteredtree.py 2010-05-29 16:16:54 +0000
+++ GTG/core/filteredtree.py 2010-05-31 15:39:30 +0000
@@ -85,6 +85,8 @@
gobject.TYPE_NONE, (str, )),
'task-deleted-inview': (gobject.SIGNAL_RUN_FIRST, \
gobject.TYPE_NONE, (str, )),
+ 'path-deleted-inview': (gobject.SIGNAL_RUN_FIRST, \
+ gobject.TYPE_NONE, (str, )),
'task-modified-inview': (gobject.SIGNAL_RUN_FIRST, \
gobject.TYPE_NONE, (str, )),}
@@ -201,7 +203,7 @@
#we remove it
if curdis:
# print "%s is removed" %tid
- self.__remove_node(tid)
+ self.__task_deleted(None, tid)
else:
# print "%s is modified, not to dis" %tid
self.emit("task-deleted-inview", tid)
@@ -654,6 +656,9 @@
self.__add_node(n,False)
def __remove_node(self,tid):
+ paths = self.get_paths_for_node(self.get_node(tid))
+ for path in paths:
+ self.emit("path-deleted-inview", path)
if tid in self.displayed_nodes:
self.remove_count += 1
self.__nodes_count -= 1
=== modified file 'GTG/core/filters_bank.py'
--- GTG/core/filters_bank.py 2010-05-23 00:52:33 +0000
+++ GTG/core/filters_bank.py 2010-05-31 15:39:30 +0000
@@ -176,6 +176,18 @@
""" Filter of tasks which are closed """
ret = task.get_status() in [Task.STA_DISMISSED, Task.STA_DONE]
return ret
+
+ def tags_intersection(self, task, tags_to_match_set):
+ '''
+ Filter that checks if two task sets intersect.
+ @param task: a task object
+ @oaram tags_to_match_set: a *set* of tag names
+ '''
+ all_tasks_tag = self.req.get_alltag_tag().get_name()
+ if all_tasks_tag in tags_to_match_set:
+ return True
+ task_tags = set([t.get_name() for t in task.get_tags()])
+ return task_tags.intersection(tags_to_match_set)
##########################################
=== modified file 'GTG/core/requester.py'
--- GTG/core/requester.py 2010-05-20 11:42:48 +0000
+++ GTG/core/requester.py 2010-05-31 15:39:30 +0000
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
-# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
#
# This program is free software: you can redistribute it and/or modify it under
@@ -39,18 +39,19 @@
Multiple L{Requester}s can exist on the same datastore, so they should
never have state of their own.
"""
- __gsignals__ = {'task-added': (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, (str, )),
- 'task-deleted': (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, (str, )),
- 'task-modified': (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, (str, )),
- 'tag-added': (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, (str, )),
- 'tag-deleted': (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, (str, )),
- 'tag-modified': (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, (str, ))}
+
+ __string_signal__ = (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str, ))
+
+ __gsignals__ = {'task-added' : __string_signal__, \
+ 'task-deleted' : __string_signal__, \
+ 'task-modified' : __string_signal__, \
+ 'task-to-be-synced': __string_signal__, \
+ 'task-tagged' : __string_signal__, \
+ 'task-untagged' : __string_signal__, \
+ 'tag-added' : __string_signal__, \
+ 'tag-deleted' : __string_signal__, \
+ 'tag-path-deleted' : __string_signal__, \
+ 'tag-modified' : __string_signal__}
def __init__(self, datastore):
"""Construct a L{Requester}."""
@@ -72,12 +73,23 @@
self.counter_call += 1
#print "signal task_modified %s (%s modifications)" %(tid,self.counter_call)
gobject.idle_add(self.emit, "task-modified", tid)
-
+
+ def _task_has_to_be_synced(self, tid):
+ #FIXME: is probably equivalent to task-modified (invernizzi)
+ gobject.idle_add(self.emit, "task-to-be-synced", tid)
+
+ def _task_deleted(self, tid):
+ #when this is emitted, task has *already* been deleted
+ gobject.idle_add(self.emit, "task-deleted", tid)
+
def _tag_added(self,tagname):
gobject.idle_add(self.emit, "tag-added", tagname)
def _tag_modified(self,tagname):
gobject.idle_add(self.emit, "tag-modified", tagname)
+
+ def _tag_path_deleted(self, path):
+ gobject.idle_add(self.emit, "tag-path-deleted", path)
############ Tasks Tree ######################
# This is the main FilteredTree. You cannot apply filters
@@ -139,6 +151,10 @@
######### Filters bank #######################
# Get the filter object for a given name
+
+ def get_filter_bank(self):
+ return self.filters
+
def get_filter(self,filter_name):
return self.filters.get_filter(filter_name)
@@ -175,7 +191,7 @@
task = self.ds.get_task(tid)
return task
- def new_task(self, pid=None, tags=None, newtask=True):
+ def new_task(self, tags=None, newtask=True):
"""Create a new task.
Note: this modifies the datastore.
@@ -188,31 +204,16 @@
existed, C{False} if importing an existing task from a backend.
@return: A task from the data store
"""
- task = self.ds.new_task(pid=pid)
+ task = self.ds.new_task()
if tags:
for t in tags:
assert(isinstance(t, Tag) == False)
task.tag_added(t)
return task
- def delete_task(self, tid):
- """Delete the task 'tid'.
-
- Note: this modifies the datastore.
-
- @param tid: The id of the task to be deleted.
- """
- #send the signal before actually deleting the task !
- Log.debug("deleting task %s" % tid)
- task = self.get_task(tid)
- for tag in task.get_tags():
- self.emit('tag-modified', tag.get_name())
- self.emit('task-deleted', tid)
- #return True
- return self.ds.delete_task(tid)
-
############### Tags ##########################
###############################################
+
def get_tag_tree(self):
return self.ds.get_tagstore()
@@ -263,3 +264,27 @@
l.append(t.get_name())
l.sort(cmp=lambda x, y: cmp(x.lower(),y.lower()))
return l
+
+ ############## Backends #######################
+ ###############################################
+
+ def get_all_backends(self, disabled = False):
+ return self.ds.get_all_backends(disabled)
+
+ def register_backend(self, dic):
+ return self.ds.register_backend(dic)
+
+ def flush_all_tasks(self, backend_id):
+ return self.ds.flush_all_tasks(backend_id)
+
+ def get_backend(self, backend_id):
+ return self.ds.get_backend(backend_id)
+
+ def set_backend_enabled(self, backend_id, state):
+ return self.ds.set_backend_enabled(backend_id, state)
+
+ def remove_backend(self, backend_id):
+ return self.ds.remove_backend(backend_id)
+
+ def backend_change_attached_tags(self, backend_id, tags):
+ return self.ds.backend_change_attached_tags(backend_id, tags)
=== modified file 'GTG/core/tagstore.py'
--- GTG/core/tagstore.py 2010-05-28 23:47:15 +0000
+++ GTG/core/tagstore.py 2010-05-31 15:39:30 +0000
@@ -40,6 +40,7 @@
# There's only one Tag store by user. It will store all the tag used
# and their attribute.
class TagStore(Tree):
+
def __init__(self,requester):
Tree.__init__(self)
@@ -48,7 +49,7 @@
### building the initial tags
# Build the "all tasks tag"
- self.alltag_tag = self.new_tag("gtg-tags-all")
+ self.alltag_tag = self.new_tag(CoreConfig.ALLTASKS_TAG)
self.alltag_tag.set_attribute("special","all")
self.alltag_tag.set_attribute("label","<span weight='bold'>%s</span>"\
% _("All tasks"))
@@ -110,6 +111,14 @@
if tagname[0] != "@":
tagname = "@" + tagname
return self.get_node(tagname)
+
+ def remove_tag(self, tag_name):
+ node = self.get_node(tag_name)
+ path = self.get_path_for_node(node)
+ print node
+ print path
+ self.req._tag_path_deleted(path)
+ self.remove_tag(tag_name)
#FIXME : also add a new filter
def rename_tag(self, oldname, newname):
@@ -137,7 +146,7 @@
tas = self.req.get_task(tid)
tas.rename_tag(oldname,newname)
#remove the old one
- self.remove_node(oldname,otag)
+ self.remove_tag(oldname,otag)
def get_all_tags_name(self, attname=None, attvalue=None):
"""Return the name of all tags
@@ -302,9 +311,12 @@
def add_task(self, tid):
if tid not in self.tasks:
self.tasks.append(tid)
- def remove_task(self,tid):
+
+ def remove_task(self, tid):
if tid in self.tasks:
self.tasks.remove(tid)
+ self.req._tag_modified(self.get_name())
+
def get_tasks(self):
#return a copy of the list
toreturn = self.tasks[:]
=== modified file 'GTG/core/task.py'
--- GTG/core/task.py 2010-05-23 00:52:33 +0000
+++ GTG/core/task.py 2010-05-31 15:39:30 +0000
@@ -50,7 +50,7 @@
self.content = ""
#self.content = \
# "<content>Press Escape or close this task to save it</content>"
- self.sync_func = None
+ self.sync_functs = []
self.title = _("My new task")
#available status are: Active - Done - Dismiss - Note
self.status = self.STA_ACTIVE
@@ -77,7 +77,6 @@
self.loaded = True
if signal:
self.req._task_loaded(self.tid)
- #not sure the following is necessary
#self.req._task_modified(self.tid)
def set_to_keep(self):
@@ -114,7 +113,7 @@
self.title = title.strip('\t\n')
else:
self.title = "(no title task)"
- #Avoid unecessary sync
+ #Avoid unnecessary sync
if self.title != old_title:
self.sync()
return True
@@ -328,8 +327,7 @@
"""Add a newly created subtask to this task. Return the task added as
a subtask
"""
- uid, pid = self.get_id().split('@') #pylint: disable-msg=W0612
- subt = self.req.new_task(pid=pid, newtask=True)
+ subt = self.req.new_task(newtask=True)
#we use the inherited childrens
self.add_child(subt.get_id())
return subt
@@ -369,7 +367,7 @@
#FIXME: what about I want to move the child to a
# root node? We have to make sure that remove_parent
# is called instead
- self.req.delete_task(tid)
+ task.delete()
self.sync()
return True
else:
@@ -463,31 +461,42 @@
#This method is called by the datastore and should not be called directly
#Use the requester
def delete(self):
- self.set_sync_func(None, callsync=False)
+ #we issue a delete for all the children
for task in self.get_subtasks():
- task.remove_parent(self.get_id())
- self.req.delete_task(task.get_id())
+ #I think it's superfluous (invernizzi)
+ #task.remove_parent(self.get_id())
+ task.delete()
+ #we tell the parents we have to go
for i in self.get_parents():
task = self.req.get_task(i)
task.remove_child(self.get_id())
+ #we tell the tags about the deletion
for tagname in self.tags:
tag = self.req.get_tag(tagname)
tag.remove_task(self.get_id())
- #then we remove effectively the task
- #self.req.delete_task(self.get_id())
+ #then we signal the we are ready to be removed
+ self.req._task_deleted(self.get_id())
#This is a callback. The "sync" function has to be set
- def set_sync_func(self, sync, callsync=True):
- self.sync_func = sync
- #We call it immediatly to save stuffs that were set before this
+ def add_sync_func(self, sync, callsync = True):
+ if sync not in self.sync_functs:
+ self.sync_functs.append(sync)
+ #We call it immediately to save stuffs that were set before this
if callsync and self.is_loaded():
self.sync()
+ def remove_sync_func(self, sync):
+ if sync in self.sync_functs:
+ self.sync_functs.remove(sync)
+
def sync(self):
self._modified_update()
- if self.sync_func and self.is_loaded():
- self.sync_func(self)
- self.call_modified()
+ if self.is_loaded():
+ self.req._task_has_to_be_synced(self.get_id())
+ #for func in self.sync_functs:
+ # func(self)
+ if self.sync_functs != []:
+ self.call_modified()
#This function send the modified signals for the tasks,
#parents and childrens
@@ -502,6 +511,9 @@
self.req._task_modified(p)
def _modified_update(self):
+ '''
+ Updates the modified timestamp
+ '''
self.modified = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
@@ -530,7 +542,7 @@
def tag_added(self, tagname):
"""
- Adds a tag. Does not add '@tag' to the contents. See insert_tag
+ Adds a tag. Does not add '@tag' to the contents. See add_tag
"""
#print "tag %s added to task %s" %(tagname,self.get_id())
t = tagname.encode("UTF-8")
@@ -541,6 +553,8 @@
#Do not add the same tag twice
if not t in self.tags:
self.tags.append(t)
+ #we notify the backends
+ #self.req.tag_was_added_to_task(self, tagname)
for child in self.get_subtasks():
if child.can_be_deleted:
child.add_tag(t)
@@ -578,6 +592,8 @@
self.req._tag_modified(tagname)
if tagname in self.tags:
self.tags.remove(tagname)
+ #we notify the backends
+ #self.req.tag_was_removed_from_task(self, tagname)
for child in self.get_subtasks():
if child.can_be_deleted:
child.remove_tag(tagname)
=== modified file 'GTG/gtg.py'
--- GTG/gtg.py 2010-05-26 09:54:42 +0000
+++ GTG/gtg.py 2010-05-31 15:39:30 +0000
@@ -51,17 +51,14 @@
import os
import dbus
import logging
-import signal
-from contextlib import contextmanager
#our own imports
-from GTG import _
-from GTG.viewmanager.manager import Manager
-from GTG.core.datastore import DataStore
-from GTG.core import CoreConfig
-from GTG.tools.logger import Log
-from GTG.tools import gtkcrashhandler
-from GTG import info
+from GTG import _
+from GTG.viewmanager.manager import Manager
+from GTG.core.datastore import DataStore
+from GTG.core import CoreConfig
+from GTG.tools.logger import Log
+from GTG.tools.gtkcrashhandler import signal_catcher
#=== OBJECTS ==================================================================
@@ -96,6 +93,23 @@
#=== MAIN CLASS ===============================================================
def main(options=None, args=None):
+ '''
+ Calling this starts the full GTG experience ( :-D )
+ '''
+ config, ds, req = core_main_init(options, args)
+ # Launch task browser
+ manager = Manager(req, config)
+ #main loop
+ #To be more user friendly and get the logs of crashes, we show an apport
+ # hooked window upon crashes
+ with signal_catcher(manager.close_browser):
+ manager.main()
+ core_main_quit(config, ds)
+
+def core_main_init(options = None, args = None):
+ '''
+ Part of the main function prior to the UI initialization.
+ '''
# Debugging subsystem initialization
if options.debug:
Log.setLevel(logging.DEBUG)
@@ -104,47 +118,30 @@
config = CoreConfig()
check_instance(config.DATA_DIR)
backends_list = config.get_backends_list()
-
- #initialize Apport hook for crash handling
- gtkcrashhandler.initialize(app_name = "Getting Things GNOME!", message = \
- "GTG" + info.VERSION + _(" has crashed. Please report the bug on <a "\
- "href=\"http://bugs.edge.launchpad.net/gtg\">our Launchpad page</a>."\
- " If you have Apport installed, it will be started for you."), \
- use_apport = True)
-
# Load data store
ds = DataStore()
-
+ # Register backends
for backend_dic in backends_list:
ds.register_backend(backend_dic)
-
#save directly the backends to be sure to write projects.xml
- config.save_datastore(ds,initial_save=True)
+ ds.save(quit = False)
# Launch task browser
req = ds.get_requester()
- manager = Manager(req, config)
-
- #we listen for signals from the system in order to save our configuration
- # if GTG is forcefully terminated (e.g.: on shutdown).
- @contextmanager
- def signal_catcher():
- #if TERM or ABORT are caught, we close the browser
- for s in [signal.SIGABRT, signal.SIGTERM]:
- signal.signal(s, lambda a,b: manager.close_browser())
- yield
+ return config, ds, req
- #main loop
- with signal_catcher():
- manager.main()
-
+def core_main_quit(config, ds):
+ '''
+ Last bits of code executed in GTG, after the UI has been shut off.
+ Currently, it's just saving everything.
+ '''
# Ideally we should load window geometry configuration from a config.
# backend like gconf at some point, and restore the appearance of the
# application as the user last exited it.
-
+ #
# Ending the application: we save configuration
- config.save_config()
- config.save_datastore(ds)
+ config.save()
+ ds.save(quit = True)
#=== EXECUTION ================================================================
=== modified file 'GTG/taskbrowser/browser.py'
--- GTG/taskbrowser/browser.py 2010-05-28 23:47:15 +0000
+++ GTG/taskbrowser/browser.py 2010-05-31 15:39:30 +0000
@@ -25,7 +25,6 @@
import pygtk
pygtk.require('2.0')
import gobject
-import os
import gtk
import locale
import re
@@ -34,26 +33,23 @@
#our own imports
import GTG
-from GTG import info
-from GTG import _
-from GTG import ngettext
-from GTG.tools.logger import Log
-from GTG.core.task import Task
-#from GTG.core.tagstore import Tag
-from GTG.taskbrowser import GnomeConfig
-from GTG.taskbrowser import tasktree
-#from GTG.taskbrowser.preferences import PreferencesDialog
-from GTG.taskbrowser.tasktree import TaskTreeModel,\
- ActiveTaskTreeView,\
- ClosedTaskTreeView
-from GTG.taskbrowser import tagtree
-from GTG.taskbrowser.tagtree import TagTreeModel,\
- TagTreeView
-from GTG.tools import openurl
-from GTG.tools.dates import strtodate,\
- no_date,\
- FuzzyDate, \
- get_canonical_date
+from GTG import info
+from GTG.core import CoreConfig
+from GTG import _
+from GTG import ngettext
+from GTG.tools.logger import Log
+from GTG.core.task import Task
+#from GTG.core.tagstore import Tag
+from GTG.taskbrowser import GnomeConfig
+from GTG.taskbrowser import tasktree
+from GTG.taskbrowser.tasktree import TaskTreeModel,\
+ActiveTaskTreeView,\
+ClosedTaskTreeView
+from GTG.taskbrowser import tagtree
+from GTG.taskbrowser.tagtree import TagTreeModel,\
+TagTreeView
+from GTG.tools import openurl
+from GTG.tools.dates import no_date,FuzzyDate, get_canonical_date
#from GTG.tools import clipboard
#=== MAIN CLASS ===============================================================
@@ -160,7 +156,7 @@
self.priv['quick_add_cbs'] = []
def _init_icon_theme(self):
- icon_dirs = [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]
+ icon_dirs = CoreConfig.get_icons_directories()
for i in icon_dirs:
gtk.icon_theme_get_default().prepend_search_path(i)
gtk.window_set_default_icon_name("gtg")
@@ -323,6 +319,8 @@
self.on_nonworkviewtag_toggled,
"on_preferences_activate":
self.open_preferences,
+ "on_edit_backends_activate":
+ self.open_edit_backends,
}
self.builder.connect_signals(SIGNAL_CONNECTIONS_DIC)
@@ -368,6 +366,11 @@
# Connect requester signals to TreeModels
self.req.connect("task-added", self.on_task_added)
self.req.connect("task-deleted", self.on_task_deleted)
+ #this causes changed be shouwn only on save
+ #tree = self.task_tree_model.get_tree()
+ #tree.connect("task-added-inview", self.on_task_added)
+ #tree.connect("task-deleted-inview", self.on_task_deleted)
+
# Connect signals from models
self.task_modelsort.connect("row-has-child-toggled",\
@@ -442,9 +445,12 @@
### HELPER FUNCTIONS ########################################################
- def open_preferences(self,widget):
- self.vmanager.show_preferences(self.priv)
+ def open_preferences(self, widget):
+ self.vmanager.open_preferences(self.priv)
+ def open_edit_backends(self, widget):
+ self.vmanager.open_edit_backends()
+
def quit(self,widget=None):
self.vmanager.close_browser()
=== modified file 'GTG/taskbrowser/taskbrowser.glade'
--- GTG/taskbrowser/taskbrowser.glade 2010-05-22 22:41:44 +0000
+++ GTG/taskbrowser/taskbrowser.glade 2010-05-31 15:39:30 +0000
@@ -11,7 +11,6 @@
<child>
<object class="GtkVBox" id="master_vbox">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
<child>
<object class="GtkMenuBar" id="browser_menu">
<property name="visible">True</property>
@@ -154,6 +153,17 @@
<signal name="activate" handler="on_preferences_activate"/>
</object>
</child>
+ <child>
+ <object class="GtkImageMenuItem" id="backends_mi">
+ <property name="label">_Backends</property>
+ <property name="visible">True</property>
+ <property name="use_underline">True</property>
+ <property name="image">image4</property>
+ <property name="use_stock">False</property>
+ <property name="accel_group">accelgroup1</property>
+ <signal name="activate" handler="on_edit_backends_activate"/>
+ </object>
+ </child>
</object>
</child>
</object>
@@ -253,6 +263,7 @@
<property name="label">gtk-help</property>
<property name="visible">True</property>
<property name="tooltip_text" translatable="yes">Open GTG documentation in your web browser</property>
+ <property name="use_underline">True</property>
<property name="use_stock">True</property>
<property name="accel_group">accelgroup1</property>
<signal name="activate" handler="on_documentation_clicked"/>
@@ -429,7 +440,6 @@
<child>
<object class="GtkVBox" id="sidebar_vbox">
<property name="width_request">75</property>
- <property name="orientation">vertical</property>
<child>
<object class="GtkHBox" id="hbox4">
<property name="visible">True</property>
@@ -484,9 +494,9 @@
<object class="GtkNotebook" id="sidebar_notebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
+ <property name="tab_pos">bottom</property>
+ <property name="show_tabs">False</property>
<property name="group_id">1</property>
- <property name="show_tabs">False</property>
- <property name="tab_pos">bottom</property>
<child>
<object class="GtkScrolledWindow" id="sidebar-scroll">
<property name="visible">True</property>
@@ -507,7 +517,6 @@
<packing>
<property name="tab_fill">False</property>
</packing>
-
</child>
</object>
<packing>
@@ -523,7 +532,6 @@
<child>
<object class="GtkVBox" id="main_vbox">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
<child>
<object class="GtkHBox" id="quickadd_pane">
<property name="visible">True</property>
@@ -567,14 +575,13 @@
<object class="GtkVPaned" id="vpaned1">
<property name="visible">True</property>
<property name="can_focus">True</property>
- <property name="orientation">vertical</property>
<child>
<object class="GtkNotebook" id="main_notebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
+ <property name="tab_pos">bottom</property>
+ <property name="show_tabs">False</property>
<property name="group_id">2</property>
- <property name="show_tabs">False</property>
- <property name="tab_pos">bottom</property>
<child>
<object class="GtkScrolledWindow" id="main_pane">
<property name="visible">True</property>
@@ -605,9 +612,9 @@
<object class="GtkNotebook" id="accessory_notebook">
<property name="visible">False</property>
<property name="can_focus">True</property>
+ <property name="tab_pos">bottom</property>
+ <property name="show_tabs">False</property>
<property name="group_id">2</property>
- <property name="show_tabs">False</property>
- <property name="tab_pos">bottom</property>
<child>
<placeholder/>
</child>
@@ -695,7 +702,6 @@
<child internal-child="vbox">
<object class="GtkVBox" id="about_dialog_vbox">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkHButtonBox" id="dialog-action_area2">
@@ -745,7 +751,6 @@
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">False</property>
- <property name="submenu">schedule_for_context_menu</property>
</object>
</child>
<child>
@@ -959,7 +964,6 @@
<child internal-child="vbox">
<object class="GtkVBox" id="addtag_dialog_vbox">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="addtag_label">
@@ -1062,4 +1066,8 @@
<property name="pixel_size">16</property>
<property name="icon_name">gtg-tag</property>
</object>
+ <object class="GtkImage" id="image4">
+ <property name="visible">True</property>
+ <property name="stock">gtk-home</property>
+ </object>
</interface>
=== modified file 'GTG/taskbrowser/tasktree.py'
--- GTG/taskbrowser/tasktree.py 2010-05-26 07:50:36 +0000
+++ GTG/taskbrowser/tasktree.py 2010-05-31 15:39:30 +0000
@@ -86,15 +86,18 @@
self.tree = tree
else:
self.tree = self.req.get_main_tasks_tree()
- self.tree.connect('task-added-inview',self.add_task)
- self.tree.connect('task-deleted-inview',self.remove_task)
- self.tree.connect('task-modified-inview',self.update_task)
+ self.tree.connect('task-added-inview', self.add_task)
+ self.tree.connect('path-deleted-inview', self.remove_task)
+ self.tree.connect('task-modified-inview', self.update_task)
#need to get the GTK style for the inline preview of task content
tempwin = gtk.Window()
tempwin.realize()
self.style = tempwin.get_style()
tempwin.destroy()
+ def get_tree(self):
+ return self.tree
+
### TREE MODEL HELPER FUNCTIONS ###############################################
def _count_active_subtasks_rec(self, task):
@@ -289,18 +292,18 @@
# print "tasktree child toogled %s" %tid
self.row_has_child_toggled(par_path, par_iter)
- def remove_task(self, sender, tid):
+ def remove_task(self, sender, string_path):
#a task has been removed from the view. Therefore,
# the widgets that represent it should be removed
- Log.debug("tasktree remove_task %s" %tid)
- node = self.tree.get_node(tid)
- removed = False
- node_paths = self.tree.get_paths_for_node(node)
- for node_path in node_paths:
- Log.debug("* tasktree REMOVE %s - %s " %(tid,node_path))
- self.row_deleted(node_path)
- removed = True
- return removed
+
+ #we must rebuild the tuple (serialized as string
+ list_path = []
+ for c in string_path[1:-1].split(","):
+ if c:
+ list_path.append(int(c))
+ path = tuple(list_path)
+ self.row_deleted(path)
+ return True
def move_task(self, parent_tid, child_tid):
"""Moves the task identified by child_tid under
=== modified file 'GTG/tests/__init__.py'
--- GTG/tests/__init__.py 2010-05-29 13:02:14 +0000
+++ GTG/tests/__init__.py 2010-05-31 15:39:30 +0000
@@ -26,6 +26,8 @@
test_taskviewserial,
test_tree,
test_apidocs,
+ test_backends,
+ test_datastore,
)
@@ -35,4 +37,6 @@
test_taskviewserial.test_suite(),
test_tree.test_suite(),
test_apidocs.test_suite(),
+ test_backends.test_suite(),
+ test_datastore.test_suite(),
])
=== added file 'GTG/tests/test_backends.py'
--- GTG/tests/test_backends.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_backends.py 2010-05-31 15:39:30 +0000
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+"""Tests for GTG backends.
+
+Some of these tests will generate files in
+xdg.BaseDirectory.xdg_data_home/gtg directory.
+"""
+
+# Standard imports
+import unittest
+import os
+import xdg
+
+# GTG imports
+from GTG.backends import backend_localfile as localfile
+from GTG.core import datastore
+from GTG.tools import cleanxml
+
+
+class GtgBackendsUniTests(unittest.TestCase):
+ """Tests for GTG backends."""
+
+ def __init__(self, test):
+ unittest.TestCase.__init__(self, test)
+ self.taskfile = ''
+ self.datafile = ''
+ self.taskpath = ''
+ self.datapath = ''
+
+ def test_localfile_get_name(self):
+ """Tests for localfile/get_name function :
+ - a string is expected.
+ """
+ res = localfile.Backend.get_name()
+ expectedres = "backend_localfile"
+ self.assertEqual(res, expectedres)
+
+ def test_localfile_get_description(self):
+ """Tests for localfile/get_description function :
+ - a string is expected.
+ """
+ res = localfile.Backend.get_description()
+ expectedres = "Your tasks are saved"
+ self.assertEqual(res[:len(expectedres)], expectedres)
+
+
+ def test_localfile_get_static_parameters(self):
+ """Tests for localfile/get_static_parameters function:
+ - a string is expected.
+ """
+ res = localfile.Backend.get_static_parameters()
+ self.assertEqual(res['path']['type'], "file")
+
+ def test_localfile_get_type(self):
+ """Tests for localfile/get_type function:
+ - a string is expected.
+ """
+ res = localfile.Backend.get_type()
+ expectedres = "readwrite"
+ self.assertEqual(res, expectedres)
+
+
+ def test_localfile_backend_method3(self):
+ """Tests for localfile/Backend/remove_task method:
+ - parse task file to check if task has been removed.
+ """
+ self.create_test_environment()
+ doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
+ xmlproject = doc.getElementsByTagName('backend')
+ for domobj in xmlproject:
+ dic = {}
+ if domobj.hasAttribute("module"):
+ dic["module"] = str(domobj.getAttribute("module"))
+ dic["pid"] = str(domobj.getAttribute("pid"))
+ dic["xmlobject"] = domobj
+ dic["path"] = self.taskpath
+ beobj = localfile.Backend(dic)
+ expectedres = True
+ beobj.remove_task("0@1")
+ beobj.quit()
+ dataline = open(self.taskpath, 'r').read()
+ print dataline
+ if "0@1" in dataline:
+ res = False
+ else:
+ res = True
+ expectedres = True
+ self.assertEqual(res, expectedres)
+
+# def test_localfile_backend_method4(self):
+# """Tests for localfile/Backend/get_task method:
+# - Compares task titles to check if method works.
+# """
+# self.create_test_environment()
+# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
+# xmlproject = doc.getElementsByTagName('backend')
+# for domobj in xmlproject:
+# dic = {}
+# if domobj.hasAttribute("module"):
+# dic["module"] = str(domobj.getAttribute("module"))
+# dic["pid"] = str(domobj.getAttribute("pid"))
+# dic["xmlobject"] = domobj
+# dic["filename"] = self.taskfile
+# beobj = localfile.Backend(dic)
+# dstore = datastore.DataStore()
+# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
+# beobj.get_task(newtask, "0@1")
+# self.assertEqual(newtask.get_title(), u"Ceci est un test")
+
+# def test_localfile_backend_method5(self):
+# """Tests for localfile/Backend/set_task method:
+# - parses task file to check if new task has been stored.
+# """
+# self.create_test_environment()
+# doc, configxml = cleanxml.openxmlfile(self.datapath, 'config')
+# xmlproject = doc.getElementsByTagName('backend')
+# for domobj in xmlproject:
+# dic = {}
+# if domobj.hasAttribute("module"):
+# dic["module"] = str(domobj.getAttribute("module"))
+# dic["pid"] = str(domobj.getAttribute("pid"))
+# dic["xmlobject"] = domobj
+# dic["filename"] = self.taskfile
+# beobj = localfile.Backend(dic)
+# dstore = datastore.DataStore()
+# newtask = dstore.new_task(tid="0@2", pid="1", newtask=True)
+# beobj.set_task(newtask)
+# dataline = open(self.taskpath, 'r').read()
+# if "0@2" in dataline:
+# res = True
+# else:
+# res = False
+# expectedres = True
+# self.assertEqual(res, expectedres)
+
+ def create_test_environment(self):
+ """Create the test environment"""
+ self.taskfile = 'test.xml'
+ self.datafile = 'projectstest.xml'
+ tasks = [
+ '<?xml version="1.0" ?>\n',
+ '<project>\n',
+ '\t<task id="0@1" status="Active" tags="">\n',
+ '\t\t<title>\n',
+ '\t\t\tCeci est un test\n',
+ '\t\t</title>\n',
+ '\t</task>\n',
+ '</project>\n',
+ ]
+ data = [
+ '<?xml version="1.0" ?>\n',
+ '<config>\n',
+ '\t<backend filename="test.xml" module="localfile" pid="1"/>\n',
+ '</config>\n',
+ ]
+ self.testdir = os.path.join(xdg.BaseDirectory.xdg_data_home, 'gtg')
+ if not os.path.exists(self.testdir):
+ os.makedirs(self.testdir)
+ self.taskpath = os.path.join(self.testdir, self.taskfile)
+ self.datapath = os.path.join(self.testdir, self.datafile)
+ open(self.taskpath, 'w').writelines(tasks)
+ open(self.datapath, 'w').writelines(data)
+
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
=== added file 'GTG/tests/test_datastore.py'
--- GTG/tests/test_datastore.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_datastore.py 2010-05-31 15:39:30 +0000
@@ -0,0 +1,348 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the datastore
+'''
+
+import unittest
+import uuid
+import random
+import time
+
+import GTG
+from GTG.core.datastore import DataStore
+from GTG.backends.genericbackend import GenericBackend
+from GTG.core import CoreConfig
+
+
+
+class TestDatastore(unittest.TestCase):
+ '''
+ Tests for the DataStore object.
+ '''
+
+
+ def setUp(self):
+ '''
+ Creates the environment for the tests
+ @returns None
+ '''
+ self.datastore = DataStore()
+ self.requester = self.datastore.get_requester()
+
+ def test_task_factory(self):
+ '''
+ Test for the task_factory function
+ '''
+ #generate a Task with a random id
+ tid = str(uuid.uuid4())
+ task = self.datastore.task_factory(tid, newtask = True)
+ self.assertTrue(isinstance(task, GTG.core.task.Task))
+ self.assertEqual(task.get_id(), tid)
+ self.assertEqual(task.is_new(), True)
+ tid = str(uuid.uuid4())
+ task = self.datastore.task_factory(tid, newtask = False)
+ self.assertEqual(task.is_new(), False)
+
+ def test_new_task_and_has_task(self):
+ '''
+ Tests the new_task function
+ '''
+ task = self.datastore.new_task()
+ tid = task.get_id()
+ self.assertTrue(isinstance(tid, str))
+ self.assertTrue(tid != '')
+ self.assertTrue(task.is_new())
+ self.assertTrue(self.datastore.has_task(tid))
+ self.assertTrue(len(self.datastore.get_all_tasks()) == 1)
+
+ def test_get_all_tasks(self):
+ '''
+ Tests the get_all_tasks function
+ '''
+ task_ids = []
+ for i in xrange(1, 10):
+ task = self.datastore.new_task()
+ task_ids.append(task.get_id())
+ return_list =self.datastore.get_all_tasks()
+ self.assertEqual(len(return_list), i)
+ task_ids.sort()
+ return_list.sort()
+ self.assertEqual(task_ids, return_list)
+
+ def test_get_task(self):
+ '''
+ Tests the get_task function
+ '''
+ self.assertEqual(self.datastore.get_task(str(uuid.uuid4())), None)
+ task = self.datastore.new_task()
+ self.assertTrue(isinstance(self.datastore.get_task(task.get_id()),
+ GTG.core.task.Task))
+ self.assertEqual(self.datastore.get_task(task.get_id()), task)
+
+
+ def test_get_tagstore(self):
+ '''
+ Tests the get_tagstore function
+ '''
+ tagstore = self.datastore.get_tagstore()
+ self.assertTrue(isinstance(tagstore, GTG.core.tagstore.TagStore))
+
+ def test_get_requester(self):
+ '''
+ Tests the get_requester function
+ '''
+ requester = self.datastore.get_requester()
+ self.assertTrue(isinstance(requester, GTG.core.requester.Requester))
+
+ def test_get_tasks_tree(self):
+ '''
+ Tests the get_tasks_tree function
+ '''
+ tasks_tree = self.datastore.get_tasks_tree()
+ self.assertTrue(isinstance(tasks_tree, GTG.core.tree.Tree))
+
+ def test_push_task(self):
+ '''
+ Tests the push_task function
+ '''
+ task_ids = []
+ for i in xrange(1, 10):
+ tid = str(uuid.uuid4())
+ if tid not in task_ids:
+ task_ids.append(tid)
+ task = self.datastore.task_factory(tid)
+ return_value1 = self.datastore.push_task(task)
+ self.assertTrue(return_value1)
+ #we do it twice, but it should be pushed only once if it's
+ # working correctly (the second should be discarded)
+ return_value2 = self.datastore.push_task(task)
+ self.assertFalse(return_value2)
+ stored_tasks = self.datastore.get_all_tasks()
+ task_ids.sort()
+ stored_tasks.sort()
+ self.assertEqual(task_ids, stored_tasks)
+
+ def test_register_backend(self):
+ '''
+ Tests the register_backend function. It also tests the
+ get_all_backends and get_backend function as a side effect
+ '''
+ #create a simple backend dictionary
+ backend = FakeBackend(enabled = True)
+ tasks_in_backend_count = int(random.random() * 20)
+ for temp in xrange(0, tasks_in_backend_count):
+ backend.fake_add_random_task()
+ backend_dic = {'backend': backend, 'pid': 'a'}
+ self.datastore.register_backend(backend_dic)
+ all_backends = self.datastore.get_all_backends(disabled = True)
+ self.assertEqual(len(all_backends), 1)
+ registered_backend = self.datastore.get_backend(backend.get_id())
+ self.assertEqual(backend.get_id(), registered_backend.get_id())
+ self.assertTrue(isinstance(registered_backend, \
+ GTG.core.datastore.TaskSource))
+ self.assertTrue(registered_backend.is_enabled())
+ self.assertEqual(registered_backend.fake_get_initialized_count(), 1)
+ #we give some time for the backend to push all its tasks
+ time.sleep(1)
+ self.assertEqual(len(self.datastore.get_all_tasks()), \
+ tasks_in_backend_count)
+
+ #same test, disabled backend
+ backend = FakeBackend(enabled = False)
+ for temp in xrange(1, int(random.random() * 20)):
+ backend.fake_add_random_task()
+ backend_dic = {'backend': backend, 'pid':'b'}
+ self.datastore.register_backend(backend_dic)
+ all_backends = self.datastore.get_all_backends(disabled = True)
+ self.assertEqual(len(all_backends), 2)
+ all_backends = self.datastore.get_all_backends(disabled = False)
+ self.assertEqual(len(all_backends), 1)
+ registered_backend = self.datastore.get_backend(backend.get_id())
+ self.assertEqual(backend.get_id(), registered_backend.get_id())
+ self.assertTrue(isinstance(registered_backend, \
+ GTG.core.datastore.TaskSource))
+ self.assertFalse(registered_backend.is_enabled())
+ self.assertEqual(registered_backend.fake_get_initialized_count(), 0)
+ #we give some time for the backend to push all its tasks (is
+ #shouldn't, since it's disabled, but we give time anyway
+ time.sleep(1)
+ self.assertEqual(len(self.datastore.get_all_tasks()), \
+ tasks_in_backend_count)
+
+ def test_set_backend_enabled(self):
+ '''
+ Tests the set_backend_enabled function
+ '''
+ enabled_backend = FakeBackend(enabled = True)
+ disabled_backend = FakeBackend(enabled = False)
+ self.datastore.register_backend({'backend': enabled_backend, \
+ 'pid': str(uuid.uuid4()), \
+ GenericBackend.KEY_DEFAULT_BACKEND: False})
+ self.datastore.register_backend({'backend': disabled_backend,\
+ 'pid': str(uuid.uuid4()), \
+ GenericBackend.KEY_DEFAULT_BACKEND: False})
+ #enabling an enabled backend
+ self.datastore.set_backend_enabled(enabled_backend.get_id(), True)
+ self.assertEqual(enabled_backend.fake_get_initialized_count(), 1)
+ self.assertTrue(enabled_backend.is_enabled())
+ #disabling a disabled backend
+ self.datastore.set_backend_enabled(disabled_backend.get_id(), False)
+ self.assertEqual(disabled_backend.fake_get_initialized_count(), 0)
+ self.assertFalse(disabled_backend.is_enabled())
+ #disabling an enabled backend
+ self.datastore.set_backend_enabled(enabled_backend.get_id(), False)
+ self.assertEqual(enabled_backend.fake_get_initialized_count(), 1)
+ self.assertFalse(enabled_backend.is_enabled())
+ #enabling a disabled backend
+ self.datastore.set_backend_enabled(disabled_backend.get_id(), True)
+ self.assertEqual(disabled_backend.fake_get_initialized_count(), 1)
+ self.assertTrue(disabled_backend.is_enabled())
+
+ def test_remove_backend(self):
+ '''
+ Tests the remove_backend function
+ '''
+ enabled_backend = FakeBackend(enabled = True)
+ disabled_backend = FakeBackend(enabled = False)
+ self.datastore.register_backend({'backend': enabled_backend, \
+ 'pid': str(uuid.uuid4()), \
+ GenericBackend.KEY_DEFAULT_BACKEND: False})
+ self.datastore.register_backend({'backend': disabled_backend,\
+ 'pid': str(uuid.uuid4()), \
+ GenericBackend.KEY_DEFAULT_BACKEND: False})
+ #removing an enabled backend
+ self.datastore.remove_backend(enabled_backend.get_id())
+ self.assertFalse(enabled_backend.is_enabled())
+ self.assertTrue(enabled_backend.fake_is_purged())
+ self.assertEqual( \
+ len(self.datastore.get_all_backends(disabled = True)), 1)
+ #removing a disabled backend
+ self.datastore.remove_backend(disabled_backend.get_id())
+ self.assertFalse(disabled_backend.is_enabled())
+ self.assertTrue(disabled_backend.fake_is_purged())
+ self.assertEqual( \
+ len(self.datastore.get_all_backends(disabled = True)), 0)
+
+ def test_flush_all_tasks(self):
+ '''
+ Tests the flush_all_tasks function
+ '''
+ #we add some tasks in the datastore
+ tasks_in_datastore_count = 10 #int(random.random() * 20)
+ for temp in xrange(0, tasks_in_datastore_count):
+ self.datastore.new_task()
+ datastore_stored_tids = self.datastore.get_all_tasks()
+ self.assertEqual(tasks_in_datastore_count, len(datastore_stored_tids))
+
+ #we enable a backend
+ backend = FakeBackend(enabled = True)
+ self.datastore.register_backend({'backend': backend, 'pid': 'a'})
+ #we wait for the signal storm to wear off
+ time.sleep(5)
+ #we sync
+ self.datastore.get_backend(backend.get_id()).sync()
+ #and we inject task in the backend
+ tasks_in_backend_count = 5 #int(random.random() * 20)
+ for temp in xrange(0, tasks_in_backend_count):
+ backend.fake_add_random_task()
+ backend_stored_tids = backend.fake_get_task_ids()
+ self.assertEqual(tasks_in_backend_count, len(backend_stored_tids))
+ self.datastore.flush_all_tasks(backend.get_id())
+ #we wait for the signal storm to wear off
+ time.sleep(2)
+ #we sync
+ self.datastore.get_backend(backend.get_id()).sync()
+ all_tasks_count = tasks_in_backend_count + tasks_in_datastore_count
+ new_datastore_stored_tids = self.datastore.get_all_tasks()
+ new_backend_stored_tids = backend.fake_get_task_ids()
+ self.assertEqual(len(new_backend_stored_tids), all_tasks_count)
+ self.assertEqual(len(new_datastore_stored_tids), all_tasks_count)
+ new_datastore_stored_tids.sort()
+ new_backend_stored_tids.sort()
+ self.assertEqual(new_backend_stored_tids, new_datastore_stored_tids)
+
+
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromTestCase(TestDatastore)
+
+
+
+class FakeBackend(object):
+ '''
+ Mimics the behavior of a simple backend. Just used for testing
+ '''
+
+ def __init__(self, enabled = True):
+ self.enabled = enabled
+ self.initialized_count = 0
+ self.tasks_ids = []
+ self.backend_id = str(uuid.uuid4())
+ self.purged = False
+
+ def is_enabled(self):
+ return self.enabled
+
+ def initialize(self):
+ self.initialized_count += 1
+ self.enabled = True
+
+ def set_task(self, task):
+ self.tasks_ids.append(task.get_id())
+
+ def has_task(self, task_id):
+ return task_id in self.tasks_ids
+
+ def remove_task(self, task_id):
+ self.tasks_ids.remove(task_id)
+
+ def get_id(self):
+ return self.backend_id
+
+ def start_get_tasks(self, push_task, task_factory):
+ for task_id in self.tasks_ids:
+ push_task(task_factory(task_id))
+
+ def quit(self, disabled = False):
+ self.enabled = not disabled
+
+ def purge(self):
+ self.purged = True
+
+ def get_attached_tags(self):
+ return [CoreConfig.ALLTASKS_TAG]
+
+ ##########################################################################
+ # The following are used just for testing, they're not present inside a
+ # normal backend
+ ##########################################################################
+
+ def fake_get_initialized_count(self):
+ return self.initialized_count
+
+ def fake_get_task_ids(self):
+ return self.tasks_ids
+
+ def fake_add_random_task(self):
+ self.tasks_ids.append(str(uuid.uuid4()))
+
+ def fake_is_purged(self):
+ return self.purged
=== added file 'GTG/tools/borg.py'
--- GTG/tools/borg.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/borg.py 2010-05-31 15:39:30 +0000
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+
+
+class Borg(object):
+ """
+ This pattern ensures that all instances of a particular class share
+ the same state (just inherit this class to have it working)
+ """
+
+ _borg_state = {}
+
+ def __init__(self):
+ self.__dict__ = self._borg_state
+
+
=== modified file 'GTG/tools/colors.py'
--- GTG/tools/colors.py 2009-12-06 22:33:19 +0000
+++ GTG/tools/colors.py 2010-05-31 15:39:30 +0000
@@ -20,7 +20,7 @@
#Take list of Tags and give the background color that should be applied
#The returned color might be None (in which case, the default is used)
-def background_color(tags, bgcolor=None):
+def background_color(tags, bgcolor = None):
if not bgcolor:
bgcolor = gtk.gdk.color_parse("#FFFFFF")
# Compute color
@@ -51,3 +51,30 @@
my_color = gtk.gdk.Color(red, green, blue).to_string()
return my_color
+
+def get_colored_tag_markup(req, tag_name):
+ '''
+ Given a tag name, returns a string containing the markup to color the
+ tag name
+ '''
+ tag = req.get_tag(tag_name)
+ if tag is None:
+ #no task loaded with that tag, color cannot be taken
+ return tag_name
+ else:
+ tag_color = tag.get_attribute("color")
+ if tag_color:
+ return '<span color="%s">%s</span>' % (tag_color, tag_name)
+ else:
+ return tag_name
+
+def get_colored_tags_markup(req, tag_names):
+ '''
+ Calls get_colored_tag_markup for each tag_name in tag_names
+ '''
+ tag_markups = map(lambda t: get_colored_tag_markup(req, t), tag_names)
+ tags_txt = ""
+ if tag_markups:
+ #reduce crashes if applied to an empty list
+ tags_txt = reduce(lambda a, b: a + ", " + b, tag_markups)
+ return tags_txt
=== modified file 'GTG/tools/gtkcrashhandler.py'
--- GTG/tools/gtkcrashhandler.py 2010-05-29 14:00:26 +0000
+++ GTG/tools/gtkcrashhandler.py 2010-05-31 15:39:30 +0000
@@ -33,6 +33,12 @@
import sys
import os
import time
+import signal
+from contextlib import contextmanager
+
+from GTG import info
+
+
try:
import pygtk
pygtk.require("2.0") # not tested on earlier versions
@@ -297,3 +303,21 @@
return "gtkcrashhandler.py should imported, not run"
raise DoNotRunException()
+
+## We handle initialization directly here, since this module will be used as a
+# singleton
+ #we listen for signals from the system in order to save our configuration
+ # if GTG is forcefully terminated (e.g.: on shutdown).
+@contextmanager
+def signal_catcher(callback):
+ #if TERM or ABORT are caught, we execute the callback function
+ for s in [signal.SIGABRT, signal.SIGTERM]:
+ signal.signal(s, lambda a,b: callback())
+ yield
+
+initialize(app_name = "Getting Things GNOME!",
+ message = "GTG" + info.VERSION +
+ _(" has crashed. Please report the bug on <a "\
+ "href=\"http://bugs.edge.launchpad.net/gtg\">our Launchpad page</a>."\
+ " If you have Apport installed, it will be started for you."), \
+ use_apport = True)
=== modified file 'GTG/viewmanager/__init__.py'
--- GTG/viewmanager/__init__.py 2010-03-02 14:31:47 +0000
+++ GTG/viewmanager/__init__.py 2010-05-31 15:39:30 +0000
@@ -25,9 +25,12 @@
import os
-from GTG import _
+
class ViewConfig:
+
+
current_rep = os.path.dirname(os.path.abspath(__file__))
DELETE_GLADE_FILE = os.path.join(current_rep, "deletion.glade")
PREFERENCES_GLADE_FILE = os.path.join(current_rep, "preferences.glade")
+ BACKENDS_GLADE_FILE = os.path.join(current_rep, "backends_dialog.glade")
=== modified file 'GTG/viewmanager/delete_dialog.py'
--- GTG/viewmanager/delete_dialog.py 2010-02-28 13:10:00 +0000
+++ GTG/viewmanager/delete_dialog.py 2010-05-31 15:39:30 +0000
@@ -39,7 +39,11 @@
"""if we pass a tid as a parameter, we delete directly
otherwise, we will look which tid is selected"""
for tid in self.tids_todelete:
- self.req.delete_task(tid)
+ task = self.req.get_task(tid)
+ if task:
+ task.delete()
+ else:
+ print "trying to delete task already deleted"
self.tids_todelete = []
def delete_tasks(self, tids=None):
=== modified file 'GTG/viewmanager/manager.py'
--- GTG/viewmanager/manager.py 2010-05-20 09:18:47 +0000
+++ GTG/viewmanager/manager.py 2010-05-31 15:39:30 +0000
@@ -26,15 +26,16 @@
import gobject
import GTG
-from GTG.viewmanager.delete_dialog import DeletionUI
-from GTG.taskbrowser.browser import TaskBrowser
-from GTG.taskeditor.editor import TaskEditor
-from GTG.viewmanager.preferences import PreferencesDialog
-from GTG.viewmanager.dbuswrapper import DBusTaskWrapper
-from GTG.tools import clipboard
-from GTG.core.plugins.engine import PluginEngine
-from GTG.core.plugins.api import PluginAPI
-from GTG.tools.logger import Log
+from GTG.viewmanager.delete_dialog import DeletionUI
+from GTG.taskbrowser.browser import TaskBrowser
+from GTG.taskeditor.editor import TaskEditor
+from GTG.viewmanager.preferences import PreferencesDialog
+#from GTG.viewmanager.backends_dialog import BackendsDialog
+from GTG.viewmanager.dbuswrapper import DBusTaskWrapper
+from GTG.tools import clipboard
+from GTG.core.plugins.engine import PluginEngine
+from GTG.core.plugins.api import PluginAPI
+from GTG.tools.logger import Log
class Manager():
@@ -69,9 +70,10 @@
#Deletion UI
self.delete_dialog = None
- #Preferences windows
- # Initialize "Preferences" dialog
- self.preferences = None
+ #Preferences and Backends windows
+ # Initialize dialogs
+ self.preferences_dialog = None
+ self.edit_backends_dialog = None
#DBus
DBusTaskWrapper(self.req, self)
@@ -186,10 +188,16 @@
################ Others dialog ############################################
- def show_preferences(self, config_priv, sender=None):
- if not self.preferences:
- self.preferences = PreferencesDialog(self.pengine, self.p_apis)
- self.preferences.activate(config_priv)
+ def open_preferences(self, config_priv, sender=None):
+ if not self.preferences_dialog:
+ self.preferences_dialog = PreferencesDialog(self.pengine, self.p_apis)
+ self.preferences_dialog.activate(config_priv)
+
+ def open_edit_backends(self, sender = None):
+ pass
+# if not self.edit_backends_dialog:
+# self.edit_backends_dialog = BackendsDialog(self.req)
+# self.edit_backends_dialog.activate()
def ask_delete_tasks(self, tids):
if not self.delete_dialog:
=== modified file 'GTG/viewmanager/preferences.glade'
--- GTG/viewmanager/preferences.glade 2010-04-29 09:23:34 +0000
+++ GTG/viewmanager/preferences.glade 2010-05-31 15:39:30 +0000
@@ -213,63 +213,10 @@
</packing>
</child>
<child>
- <object class="GtkAlignment" id="prefs-alignment3">
- <property name="visible">True</property>
- <property name="top_padding">10</property>
- <property name="bottom_padding">10</property>
- <property name="left_padding">10</property>
- <property name="right_padding">10</property>
- <child>
- <object class="GtkVBox" id="prefs-vbox5">
- <property name="visible">True</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkLabel" id="prefs-label6">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">Task _Backends:</property>
- <property name="use_underline">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkScrolledWindow" id="prefs-scrolledwindow1">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <property name="vscrollbar_policy">automatic</property>
- <child>
- <object class="GtkTreeView" id="BackendTree">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can_focus">True</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
+ <placeholder/>
</child>
<child type="tab">
- <object class="GtkLabel" id="prefs-label2">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Storage</property>
- </object>
- <packing>
- <property name="position">1</property>
- <property name="tab_fill">False</property>
- </packing>
+ <placeholder/>
</child>
<child>
<object class="GtkAlignment" id="prefs-alignment4">
=== modified file 'scripts/debug.sh'
--- scripts/debug.sh 2010-05-19 23:21:09 +0000
+++ scripts/debug.sh 2010-05-31 15:39:30 +0000
@@ -3,15 +3,17 @@
args=""
set="default"
norun=0
+profiling=0
# Create execution-time data directory if needed
mkdir -p tmp
# Interpret arguments
-while getopts dns: o
+while getopts dnps: o
do case "$o" in
d) args="-d";;
n) norun=1;;
+ p) profiling=1;;
s) set="$OPTARG";;
[?]) echo >&2 "Usage: $0 [-s dataset] [-d] [-n]"
exit 1;;
@@ -38,6 +40,10 @@
fi
if [ $norun -eq 0 ]; then
+ if [ $profiling -eq 0 ]; then
./gtg $args
+ else
+ python -m cProfile -o gtg.prof ./gtg
+ fi
fi
=== added file 'scripts/profile_interpret.sh'
--- scripts/profile_interpret.sh 1970-01-01 00:00:00 +0000
+++ scripts/profile_interpret.sh 2010-05-31 15:39:30 +0000
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+import pstats
+p = pstats.Stats('gtg.prof')
+p.strip_dirs().sort_stats("cumulative").print_stats(20)