gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #02746
[Merge] lp:~gtg-user/gtg/multibackends-halfgsoc_merge into lp:gtg
Luca Invernizzi has proposed merging lp:~gtg-user/gtg/multibackends-halfgsoc_merge into lp:gtg.
Requested reviews:
Gtg developers (gtg)
It's the last merge request [0] with all the changes we talked about applied.
It seems quite stable, since I had the need to change it rarely.
The other backends will come in separate merges, once this one is accepted.
[0] https://code.edge.launchpad.net/~gtg-user/gtg/backends-first-merge/+merge/26532
--
https://code.launchpad.net/~gtg-user/gtg/multibackends-halfgsoc_merge/+merge/28258
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/multibackends-halfgsoc_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-06-23 01:19:23 +0000
@@ -23,14 +23,173 @@
(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
+from GTG.core import firstrun_tasks
+from GTG.tools import cleanxml
+from GTG.core import CoreConfig
+
+
+
+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, additional_parameters = {}):
+ '''
+ 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
+ parameters = module.Backend.get_static_parameters()
+ #we all the parameters and their default values in dic
+ for param_name, param_dic in parameters.iteritems():
+ dic[param_name] = param_dic[GenericBackend.PARAM_DEFAULT_VALUE]
+ dic["pid"] = str(uuid.uuid4())
+ dic["module"] = module.Backend.get_name()
+ for param_name, param_value in additional_parameters.iteritems():
+ dic[param_name] = param_value
+ dic["backend"] = module.Backend(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"]
+
+ def get_saved_backends_list(self):
+ 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["pid"] = str(uuid.uuid4())
+ dic["need_conversion"] = \
+ dic["xmlobject"].getAttribute("filename")
+
+ #Now that the backend list is build, we will construct them
+ for dic in backends_dic:
+ self.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 = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
+ doc, configxml = cleanxml.openxmlfile(datafile, "config")
+ xmlproject = doc.getElementsByTagName("backend")
+ # collect configured backends
+ return [{"xmlobject": xp, \
+ "module": xp.getAttribute("module")} for xp in xmlproject]
=== 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-06-23 01:19:23 +0000
@@ -0,0 +1,181 @@
+# -*- 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.backends.genericbackend import GenericBackend
+from GTG.core import CoreConfig
+from GTG.tools import cleanxml, taskxml
+from GTG import _
+
+
+
+class Backend(GenericBackend):
+
+
+ DEFAULT_PATH = CoreConfig().get_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_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_STRING, \
+ 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 = []
+ #####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
+ ####
+ self.doc, self.xmlproj = cleanxml.openxmlfile( \
+ self._parameters["path"], "project")
+
+ def initialize(self):
+ super(Backend, self).initialize()
+ self.doc, self.xmlproj = cleanxml.openxmlfile( \
+ self._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):
+ '''
+ Once this function is launched, the backend can start pushing
+ tasks to gtg parameters.
+
+ @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 = self.datastore.task_factory(tid)
+ if task:
+ task = taskxml.task_from_xml(task, node)
+ self.datastore.push_task(task)
+
+ def set_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):
+ ''' 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 quit(self, disable = False):
+ '''
+ Called when GTG quits or disconnects the backend.
+ '''
+ super(Backend, self).quit(disable)
+
+ def save_state(self):
+ 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-06-23 01:19:23 +0000
@@ -0,0 +1,125 @@
+# -*- 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
+ '''
+
+ #error codes to send along with the BACKEND_FAILED signal
+ ERRNO_AUTHENTICATION = "authentication failed"
+ ERRNO_NETWORK = "network is down"
+ ERRNO_DBUS = "Dbus interface cannot be connected"
+
+ def __init__(self):
+ super(BackendSignals, self).__init__()
+ if hasattr(self, "_gobject"):
+ return
+ self._gobject = _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
+ DEFAULT_BACKEND_LOADED = 'default-backend-loaded' #emitted after all
+ # tasks have been
+ # loaded from the
+ # default backend
+ BACKEND_FAILED = 'backend-failed' #something went wrong with a backend
+ BACKEND_SYNC_STARTED = 'backend-sync-started'
+ BACKEND_SYNC_ENDED = 'backend-sync-ended'
+
+ __string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
+ gobject.TYPE_NONE, (str, ))
+ __none_signal__ = (gobject.SIGNAL_RUN_FIRST, \
+ gobject.TYPE_NONE, ( ))
+ __string_string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
+ gobject.TYPE_NONE, (str, str, ))
+
+ __gsignals__ = {BACKEND_STATE_TOGGLED : __string_signal__, \
+ BACKEND_RENAMED : __string_signal__, \
+ BACKEND_ADDED : __string_signal__, \
+ BACKEND_REMOVED : __string_signal__, \
+ BACKEND_SYNC_STARTED : __string_signal__, \
+ BACKEND_SYNC_ENDED : __string_signal__, \
+ DEFAULT_BACKEND_LOADED: __none_signal__, \
+ BACKEND_FAILED : __string_string_signal__}
+
+ def __init__(self):
+ super(_BackendSignalsGObject, self).__init__()
+ self.backends_currently_syncing = []
+
+ ############# 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)
+
+ def default_backend_loaded(self):
+ gobject.idle_add(self.emit, self.DEFAULT_BACKEND_LOADED)
+
+ def backend_failed(self, backend_id, error_code):
+ gobject.idle_add(self.emit, self.BACKEND_FAILED, backend_id, \
+ error_code)
+
+ def backend_sync_started(self, backend_id):
+ self._emit_signal(self.BACKEND_SYNC_STARTED, backend_id)
+ self.backends_currently_syncing.append(backend_id)
+
+ def backend_sync_ended(self, backend_id):
+ self._emit_signal(self.BACKEND_SYNC_ENDED, backend_id)
+ try:
+ self.backends_currently_syncing.remove(backend_id)
+ except:
+ pass
+
+ def is_backend_syncing(self, backend_id):
+ return backend_id in self.backends_currently_syncing
=== added file 'GTG/backends/genericbackend.py'
--- GTG/backends/genericbackend.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/genericbackend.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,571 @@
+# -*- 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!
+'''
+
+import os
+import sys
+import errno
+import pickle
+import threading
+from collections import deque
+
+from GTG.backends.backendsignals import BackendSignals
+from GTG.tools.keyring import Keyring
+from GTG.core import CoreConfig
+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_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_TYPE = "type"
+ #PARAM_TYPE is one of the following (changing this changes the way
+ # the user can configure the parameter)
+ 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
+ TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
+ # prohibited in strings
+ _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()
+ '''
+ for module_name in self.get_required_modules():
+ sys.modules[module_name]= __import__(module_name)
+ self._parameters[self.KEY_ENABLED] = True
+ self._is_initialized = True
+ #we signal that the backend has been enabled
+ self._signal_manager.backend_state_changed(self.get_id())
+
+ def start_get_tasks(self):
+ '''
+ Once this function is launched, the backend can start pushing
+ tasks to gtg parameters.
+
+ @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.
+ '''
+ pass
+
+ def remove_task(self, tid):
+ ''' Completely remove the task with ID = tid '''
+ pass
+
+ 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()
+
+ @staticmethod
+ def get_required_modules():
+ return []
+
+ 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
+ '''
+ self._is_initialized = False
+ if disable:
+ self._parameters[self.KEY_ENABLED] = False
+ #we signal that we have been disabled
+ self._signal_manager.backend_state_changed(self.get_id())
+ self._signal_manager.backend_sync_ended(self.get_id())
+ syncing_thread = threading.Thread(target = self.sync).run()
+
+ def save_state(self):
+ '''
+ It's the last function executed on a quitting backend, after the
+ pending actions have been done.
+ Useful to ensure that the state is saved in a consistent manner
+ '''
+ pass
+
+###############################################################################
+###### 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 = "attached-tags"
+ KEY_USER = "user"
+ KEY_PID = "pid"
+ ALLTASKS_TAG = "gtg-tags-all" #IXME: moved here to avoid circular imports
+
+ _static_parameters_obligatory = { \
+ KEY_DEFAULT_BACKEND: { \
+ PARAM_TYPE: TYPE_BOOL, \
+ PARAM_DEFAULT_VALUE: False, \
+ }, \
+ KEY_HUMAN_NAME: { \
+ PARAM_TYPE: TYPE_STRING, \
+ PARAM_DEFAULT_VALUE: "", \
+ }, \
+ KEY_USER: { \
+ PARAM_TYPE: TYPE_STRING, \
+ PARAM_DEFAULT_VALUE: "", \
+ }, \
+ KEY_PID: { \
+ PARAM_TYPE: TYPE_STRING, \
+ PARAM_DEFAULT_VALUE: "", \
+ }, \
+ KEY_ENABLED: { \
+ PARAM_TYPE: TYPE_BOOL, \
+ PARAM_DEFAULT_VALUE: False, \
+ }}
+
+ _static_parameters_obligatory_for_rw = { \
+ KEY_ATTACHED_TAGS: {\
+ PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
+ PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
+ }}
+
+ #Handy dictionary used in type conversion (from string to type)
+ _type_converter = {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_obligatory.copy()
+ if cls._general_description[cls.BACKEND_TYPE] == cls.TYPE_READWRITE:
+ for key, value in \
+ cls._static_parameters_obligatory_for_rw.iteritems():
+ temp_dic[key] = value
+ for key, value in cls._static_parameters.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 and \
+ self._general_description[self.BACKEND_TYPE] \
+ == self.TYPE_READWRITE):
+ parameters[self.KEY_ATTACHED_TAGS] = [self.ALLTASKS_TAG]
+ self._parameters = parameters
+ self._signal_manager = BackendSignals()
+ self._is_initialized = False
+ if Log.is_debugging_mode():
+ self.timer_timestep = 5
+ else:
+ self.timer_timestep = 1
+ self.to_set_timer = None
+ self.please_quit = False
+ self.to_set = deque()
+ self.to_remove = deque()
+
+ 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]
+ try:
+ return self._parameters[self.KEY_ATTACHED_TAGS]
+ except:
+ return []
+
+ def set_attached_tags(self, tags):
+ '''
+ Changes the set of attached tags
+ '''
+ self._parameters[self.KEY_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
+
+ def set_parameter(self, parameter, value):
+ self._parameters[parameter] = value
+
+ @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.
+ '''
+ #FIXME: we could use pickle (dumps and loads), at least in some cases
+ # (invernizzi)
+ 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)
+ elif param_type == cls.TYPE_PASSWORD:
+ if param_value == -1:
+ return None
+ return Keyring().get_password(int(param_value))
+ elif param_type == cls.TYPE_LIST_OF_STRINGS:
+ the_list = param_value.split(",")
+ if not isinstance(the_list, list):
+ the_list = [the_list]
+ return the_list
+ else:
+ raise NotImplemented("I don't know what type is '%s'" %
+ param_type)
+
+ def cast_param_type_to_string(self, param_type, param_value):
+ '''
+ Inverse of cast_param_type_from_string
+ '''
+ if param_type == GenericBackend.TYPE_PASSWORD:
+ if param_value == None:
+ return str(-1)
+ else:
+ return str(Keyring().set_password(
+ "GTG stored password -" + self.get_id(), param_value))
+ elif param_type == GenericBackend.TYPE_LIST_OF_STRINGS:
+ if param_value == []:
+ return ""
+ return reduce(lambda a, b: a + "," + b, param_value)
+ else:
+ return str(param_value)
+
+ 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
+ '''
+ 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]
+
+ def is_initialized(self):
+ '''
+ Returns if the backend is up and running
+ '''
+ return self._is_initialized
+
+ def get_parameter_type(self, param_name):
+ try:
+ return self.get_static_parameters()[param_name][self.PARAM_TYPE]
+ except KeyError:
+ return None
+
+ def register_datastore(self, datastore):
+ self.datastore = datastore
+
+###############################################################################
+### HELPER FUNCTIONS ##########################################################
+###############################################################################
+
+ def _store_pickled_file(self, path, data):
+ '''
+ A helper function to save some object in a file.
+ @param path: a relative path. A good choice is
+ "backend_name/object_name"
+ @param data: the object
+ '''
+ path = os.path.join(CoreConfig().get_data_dir(), path)
+ #mkdir -p
+ try:
+ os.makedirs(os.path.dirname(path))
+ except OSError, exception:
+ if exception.errno != errno.EEXIST:
+ raise
+ #saving
+ #try:
+ with open(path, 'wb') as file:
+ pickle.dump(data, file)
+ #except pickle.PickleError:
+ #pass
+
+ def _load_pickled_file(self, path, default_value = None):
+ '''
+ A helper function to load some object from a file.
+ @param path: the relative path of the file
+ @param default_value: the value to return if the file is missing or
+ corrupt
+ @returns object: the needed object, or default_value
+ '''
+ path = os.path.join(CoreConfig().get_data_dir(), path)
+ if not os.path.exists(path):
+ return default_value
+ else:
+ try:
+ with open(path, 'r') as file:
+ return pickle.load(file)
+ except pickle.PickleError:
+ return default_value
+
+###############################################################################
+### THREADING #################################################################
+###############################################################################
+
+ def __try_launch_setting_thread(self):
+ '''
+ Helper function to launch the setting thread, if it's not running.
+ '''
+ if self.to_set_timer == None and self.is_enabled():
+ self.to_set_timer = threading.Timer(self.timer_timestep, \
+ self.launch_setting_thread)
+ self.to_set_timer.start()
+
+ def launch_setting_thread(self):
+ '''
+ This function is launched as a separate thread. Its job is to perform
+ the changes that have been issued from GTG core. In particular, for
+ each task in the self.to_set queue, a task has to be modified or to be
+ created (if the tid is new), and for each task in the self.to_remove
+ queue, a task has to be deleted
+ '''
+ while not self.please_quit:
+ try:
+ task = self.to_set.pop()
+ except IndexError:
+ break
+ #time.sleep(4)
+ tid = task.get_id()
+ if tid not in self.to_remove:
+ self.set_task(task)
+
+ while not self.please_quit:
+ try:
+ tid = self.to_remove.pop()
+ except IndexError:
+ break
+ self.remove_task(tid)
+ #we release the weak lock
+ self.to_set_timer = None
+
+ def queue_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 queue_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 sync(self):
+ '''
+ Helper method. Forces the backend to perform all the pending changes.
+ It is usually called upon quitting the backend.
+ '''
+ #FIXME: this function should become part of the r/w r/o generic class
+ # for backends
+ if self.to_set_timer != None:
+ self.please_quit = True
+ try:
+ self.to_set_timer.cancel()
+ except:
+ pass
+ try:
+ self.to_set_timer.join(5)
+ except:
+ pass
+ self.please_quit = False
+ self.launch_setting_thread()
+ self.save_state()
+
=== 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-06-23 01:19:23 +0000
@@ -41,136 +41,80 @@
#=== 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
-
-class CoreConfig:
+from GTG.tools.testingmode import TestingMode
+
+import GTG
+from GTG.tools.logger import Log
+from GTG.tools.borg import Borg
+
+
+
+class CoreConfig(Borg):
+
#The projects and tasks are of course DATA !
#We then use XDG_DATA for them
#Don't forget the "/" at the end.
- DATA_DIR = os.path.join(xdg_data_home,'gtg/')
DATA_FILE = "projects.xml"
- CONF_DIR = os.path.join(xdg_config_home,'gtg/')
CONF_FILE = "gtg.conf"
TASK_CONF_FILE = "tasks.conf"
conf_dict = None
#DBUS
BUSNAME = "org.GTG"
BUSINTERFACE = "/org/GTG"
+ #TAGS
+ ALLTASKS_TAG = "gtg-tags-all"
def __init__(self):
- if not os.path.exists(self.CONF_DIR):
- os.makedirs(self.CONF_DIR)
- if not os.path.exists(self.DATA_DIR):
- os.makedirs(self.DATA_DIR)
- if not os.path.exists(self.CONF_DIR + self.CONF_FILE):
- f = open(self.CONF_DIR + self.CONF_FILE, "w")
- f.close()
- if not os.path.exists(self.CONF_DIR + self.TASK_CONF_FILE):
- f = open(self.CONF_DIR + self.TASK_CONF_FILE, "w")
- f.close()
- for file in [self.CONF_DIR + self.CONF_FILE,
- self.CONF_DIR + self.TASK_CONF_FILE]:
+ if hasattr(self, 'data_dir'):
+ #Borg has already been initialized
+ return
+ if TestingMode().get_testing_mode():
+ #we avoid running tests in the user data dir
+ self.data_dir = '/tmp/GTG_TESTS/data'
+ self.conf_dir = '/tmp/GTG_TESTS/conf'
+ else:
+ self.data_dir = os.path.join(xdg_data_home,'gtg/')
+ self.conf_dir = os.path.join(xdg_config_home,'gtg/')
+ if not os.path.exists(self.conf_dir):
+ os.makedirs(self.conf_dir)
+ if not os.path.exists(self.data_dir):
+ os.makedirs(self.data_dir)
+ if not os.path.exists(self.conf_dir + self.CONF_FILE):
+ f = open(self.conf_dir + self.CONF_FILE, "w")
+ f.close()
+ if not os.path.exists(self.conf_dir + self.TASK_CONF_FILE):
+ f = open(self.conf_dir + self.TASK_CONF_FILE, "w")
+ f.close()
+ for file in [self.conf_dir + self.CONF_FILE,
+ self.conf_dir + self.TASK_CONF_FILE]:
if not ((file, os.R_OK) and os.access(file, os.W_OK)):
raise Exception("File " + file + \
- " is a configuration file for gtg, but it " + \
+ " is a configuration file for gtg, but it "
"cannot be read or written. Please check it")
- self.conf_dict = ConfigObj(self.CONF_DIR + self.CONF_FILE)
- self.task_conf_dict = ConfigObj(self.CONF_DIR + self.TASK_CONF_FILE)
+ 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()
-
- 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)
-
- # 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
- 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()
+
+ def get_icons_directories(self):
+ '''
+ Returns the directories containing the icons
+ '''
+ return [GTG.DATA_DIR, os.path.join(GTG.DATA_DIR, "icons")]
+
+ def get_data_dir(self):
+ return self.data_dir
+
+ def set_data_dir(self, path):
+ self.data_dir = path
+
+ def get_conf_dir(self):
+ return self.conf_dir
+
+ def set_conf_dir(self, path):
+ self.conf_dir = path
=== modified file 'GTG/core/datastore.py'
--- GTG/core/datastore.py 2010-05-26 08:55:45 +0000
+++ GTG/core/datastore.py 2010-06-23 01:19:23 +0000
@@ -18,267 +18,560 @@
# -----------------------------------------------------------------------------
"""
-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.tools.keyring import Keyring
+from GTG.backends.backendsignals import BackendSignals
+from GTG.tools.synchronized import synchronized
+from GTG.tools.borg import Borg
+
+
+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()
+ self.is_default_backend_loaded = False
+ self._backend_signals.connect('default-backend-loaded', \
+ self._activate_non_default_backends)
+ self.filtered_datastore = FilteredDataStore(self)
+
+ ##########################################################################
+ ### 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"
+
+ @synchronized
+ 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
+ '''
+
+ if self.has_task(task.get_id()):
+ 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"
+ if self.is_default_backend_loaded:
+ task.sync()
+ 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 None
+ backend = backend_dic["backend"]
+ #Checking that is a new backend
+ if backend.get_id() in self.backends:
+ Log.debug("registering already registered backend")
+ return None
+ source = TaskSource(requester = self.requester,
+ backend = backend,
+ datastore = self.filtered_datastore)
+ 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)
+ #this is useful for retro-compatibility
+ if not GenericBackend.KEY_ENABLED in backend_dic:
+ source.set_parameter(GenericBackend.KEY_ENABLED, True)
+ if not GenericBackend.KEY_DEFAULT_BACKEND in backend_dic:
+ source.set_parameter(GenericBackend.KEY_DEFAULT_BACKEND, True)
+ #if it's enabled, we initialize it
+ if source.is_enabled() and \
+ (self.is_default_backend_loaded or source.is_default()):
+ 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()
+ return source
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 _activate_non_default_backends(self, sender = None):
+ '''
+ Non-default backends have to wait until the default loads before
+ being activated. This function is called after the first default
+ backend has loaded all its tasks.
+ '''
+ if self.is_default_backend_loaded:
+ Log.debug("spurious call")
+ return
+ self.is_default_backend_loaded = True
+ for backend in self.backends.itervalues():
+ if backend.is_enabled() and not backend.is_default():
+ backend.initialize()
+ backend.start_get_tasks()
+ self.flush_all_tasks(backend.get_id())
+
+ 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:
+ if self.is_default_backend_loaded == True:
+ backend.initialize()
+ self.flush_all_tasks(backend_id)
+ else:
+ #will be activated afterwards
+ backend.set_parameter(GenericBackend.KEY_ENABLED,
+ True)
+
+ 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():
+ backend.queue_set_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):
+ t_xml = doc.createElement("backend")
+ for key, value in b.get_parameters().iteritems():
+ if key in ["backend", "xmlobject"]:
+ #We don't want parameters,backend,xmlobject
+ continue
+ param_type = b.get_parameter_type(key)
+ value = b.cast_param_type_to_string(param_type, value)
+ t_xml.setAttribute(str(key), value)
+ #Saving all the projects at close
+ xmlconfig.appendChild(t_xml)
+
+ datafile = os.path.join(CoreConfig().get_data_dir(), CoreConfig.DATA_FILE)
+ cleanxml.savexml(datafile,doc,backup=True)
+
+ #Saving the tagstore
+ ts = self.get_tagstore()
+ ts.save()
+
+ def request_task_deletion(self, tid):
+ '''
+ This is a proxy function to request a task deletion from a backend
+ @param tid: the tid of the task to remove
+ '''
+ self.requester.delete_task(tid)
+
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, datastore):
"""
Instantiates a TaskSource object.
- @param backend: (Required) Task Backend being wrapperized
- @param parameters: Dictionary of custom parameters.
+ @param requester: a Requester
+ @param backend: the backend being wrapped
+ @param datastore: a FilteredDatastore
"""
self.backend = backend
- self.dic = parameters
- self.to_set = []
- self.to_remove = []
- self.lock = threading.Lock()
- self.count_set = 0
+ self.req = requester
+ self.backend.register_datastore(datastore)
+ self.to_set = deque()
+ self.to_remove = deque()
+ self.task_filter = self.get_task_filter_for_backend()
+ if Log.is_debugging_mode():
+ self.timer_timestep = 5
+ else:
+ self.timer_timestep = 1
+ 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.start()
-
- def set_task(self, task):
+ '''
+ threading.Thread(target = self.__start_get_tasks).start()
+
+ def __start_get_tasks(self):
+ '''
+ Loads all task from the backend and connects its signals afterwards.
+ Launched as a thread by start_get_tasks
+ '''
+ self.backend.start_get_tasks()
+ self._connect_signals()
+ if self.backend.is_default():
+ BackendSignals().default_backend_loaded()
+
+ 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("backend_filter").get_function()
+ 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 queue_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.queue_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.queue_set_task(task)
+ while True:
+ try:
+ tid = self.to_remove.pop()
+ except IndexError:
+ break
+ self.backend.queue_remove_task(tid)
+ #we release the weak lock
+ self.to_set_timer = None
- def remove_task(self, tid):
- """
+ def queue_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(self.timer_timestep, \
+ 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-modified', \
+ self.queue_set_task)
+ if not self.remove_task_handle:
+ self.remove_task_handle = self.req.connect('task-deleted',\
+ self.queue_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()
+ except:
+ pass
+ try:
+ self.to_set_timer.join(5)
+ 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)
+
+
+
+class FilteredDataStore(Borg):
+ '''
+ This class acts as an interface to the Datastore.
+ It is used to hide most of the methods of the Datastore.
+ The backends can safely use the remaining methods.
+ '''
+
+
+ def __init__(self, datastore):
+ super(FilteredDataStore, self).__init__()
+ self.datastore = datastore
+
+ def __getattr__(self, attr):
+ if attr in ['task_factory', \
+ 'push_task',
+ 'get_task',
+ 'has_task',
+ 'request_task_deletion']:
+ return getattr(self.datastore, attr)
+ else:
+ raise AttributeError
+
=== modified file 'GTG/core/filters_bank.py'
--- GTG/core/filters_bank.py 2010-06-14 19:30:50 +0000
+++ GTG/core/filters_bank.py 2010-06-23 01:19:23 +0000
@@ -35,6 +35,10 @@
def set_parameters(self,dic):
self.dic = dic
+
+ def get_function(self):
+ '''Returns the filtering function'''
+ return self.func
def is_displayed(self,tid):
task = self.req.get_task(tid)
@@ -152,6 +156,9 @@
#worklate
filt_obj = Filter(self.worklate,self.req)
self.available_filters['worklate'] = filt_obj
+ #backend filter
+ filt_obj = Filter(self.backend_filter, self.req)
+ self.available_filters['backend_filter'] = filt_obj
#no_disabled_tag
filt_obj = Filter(self.no_disabled_tag,self.req)
param = {}
@@ -234,6 +241,19 @@
""" Filter of tasks which are closed """
ret = task.get_status() in [Task.STA_DISMISSED, Task.STA_DONE]
return ret
+
+ def backend_filter(self, task, tags_to_match_set):
+ '''
+ Filter that checks if two tags sets intersect. It is used to check if a
+ task should be stored inside a backend
+ @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(task.get_tags_name())
+ return task_tags.intersection(tags_to_match_set)
def no_disabled_tag(self,task,parameters=None):
"""Filter of task that don't have any disabled/nonworkview tag"""
=== modified file 'GTG/core/requester.py'
--- GTG/core/requester.py 2010-06-21 12:34:23 +0000
+++ GTG/core/requester.py 2010-06-23 01:19:23 +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,18 @@
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-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 +72,19 @@
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_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)
def _tag_deleted(self,tagname):
gobject.idle_add(self.emit, "tag-deleted", tagname)
@@ -126,6 +133,7 @@
######### Filters bank #######################
# Get the filter object for a given name
+
def get_filter(self,filter_name):
return self.filters.get_filter(filter_name)
@@ -162,7 +170,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.
@@ -175,11 +183,12 @@
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)
+ self._task_loaded(task.get_id())
return task
def delete_task(self, tid):
@@ -196,11 +205,11 @@
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)
+ return self.basetree.remove_node(tid)
############### Tags ##########################
###############################################
+
def get_tag_tree(self):
return self.ds.get_tagstore()
@@ -251,3 +260,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-06-12 13:31:18 +0000
+++ GTG/core/tagstore.py 2010-06-23 01:19:23 +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)
@@ -50,7 +51,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"))
@@ -68,7 +69,7 @@
self.sep_tag.set_attribute("special","sep")
self.sep_tag.set_attribute("order",2)
- self.filename = os.path.join(CoreConfig.DATA_DIR, XMLFILE)
+ self.filename = os.path.join(CoreConfig().get_data_dir(), XMLFILE)
doc, self.xmlstore = cleanxml.openxmlfile(self.filename,
XMLROOT) #pylint: disable-msg=W0612
for t in self.xmlstore.childNodes:
@@ -121,7 +122,7 @@
if tagname[0] != "@":
tagname = "@" + tagname
return self.get_node(tagname)
-
+
#FIXME : also add a new filter
def rename_tag(self, oldname, newname):
if len(newname) > 0 and \
@@ -316,9 +317,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-06-18 16:36:17 +0000
+++ GTG/core/task.py 2010-06-23 01:19:23 +0000
@@ -48,10 +48,10 @@
#tid is a string ! (we have to choose a type and stick to it)
self.tid = str(ze_id)
self.set_uuid(uuid.uuid4())
+ self.remote_ids = {}
self.content = ""
#self.content = \
# "<content>Press Escape or close this task to save it</content>"
- self.sync_func = None
self.title = _("My new task")
#available status are: Active - Done - Dismiss - Note
self.status = self.STA_ACTIVE
@@ -78,7 +78,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):
@@ -102,6 +101,25 @@
self.sync()
return self.uuid
+ def get_remote_ids(self):
+ '''
+ A task usually has a different id in all the different backends.
+ This function returns a dictionary backend_id->the id the task has
+ in that backend
+ @returns dict: dictionary backend_id->task remote id
+ '''
+ return self.remote_ids
+
+ def add_remote_id(self, backend_id, task_remote_id):
+ '''
+ A task usually has a different id in all the different backends.
+ This function adds a relationship backend_id-> remote_id that can be
+ retrieved using get_remote_ids
+ @param backend_id: string representing the backend id
+ @param task_remote_id: the id for this task in the backend backend_id
+ '''
+ self.remote_ids[str(backend_id)] = str(task_remote_id)
+
def get_title(self):
return self.title
@@ -115,7 +133,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
@@ -294,8 +312,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
@@ -427,37 +444,32 @@
#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())
-
- #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
- if callsync and self.is_loaded():
- self.sync()
+ #then we signal the we are ready to be removed
+ self.req._task_deleted(self.get_id())
def sync(self):
self._modified_update()
- if self.sync_func and self.is_loaded():
- self.sync_func(self)
+ if self.is_loaded():
self.call_modified()
return True
else:
return False
#This function send the modified signals for the tasks,
- #parents and childrens
+ #parents and children
def call_modified(self):
#we first modify children
for s in self.get_children():
@@ -469,10 +481,11 @@
self.req._task_modified(p)
def _modified_update(self):
+ '''
+ Updates the modified timestamp
+ '''
self.modified = datetime.now()
-
-
### TAG FUNCTIONS ############################################################
#
def get_tags_name(self):
@@ -509,6 +522,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)
=== modified file 'GTG/gtg.py'
--- GTG/gtg.py 2010-06-18 11:55:03 +0000
+++ GTG/gtg.py 2010-06-23 01:19:23 +0000
@@ -45,18 +45,17 @@
"""This is the top-level exec script for running GTG"""
#=== IMPORT ===================================================================
-from contextlib import contextmanager
import os
import logging
-import signal
import dbus
#our own imports
-from GTG import _, info
+from GTG.backends import BackendFactory
+from GTG import _
from GTG.core import CoreConfig
from GTG.core.datastore import DataStore
-from GTG.gtk import crashhandler
+from GTG.gtk.crashhandler import signal_catcher
from GTG.gtk.manager import Manager
from GTG.tools.logger import Log
@@ -93,54 +92,55 @@
#=== 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)
Log.debug("Debug output enabled.")
-
+ Log.set_debugging_mode(True)
config = CoreConfig()
- check_instance(config.DATA_DIR)
- backends_list = config.get_backends_list()
-
- #initialize Apport hook for crash handling
- crashhandler.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)
-
+ check_instance(config.get_data_dir())
+ backends_list = BackendFactory().get_saved_backends_list()
# 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/gtk/browser/browser.py'
--- GTG/gtk/browser/browser.py 2010-06-21 12:34:23 +0000
+++ GTG/gtk/browser/browser.py 2010-06-23 01:19:23 +0000
@@ -23,7 +23,6 @@
#=== IMPORT ===================================================================
#system imports
import locale
-import os
import re
import time
import webbrowser
@@ -35,18 +34,16 @@
#our own imports
import GTG
+from GTG.core import CoreConfig
from GTG import _, info, ngettext
from GTG.core.task import Task
-#from GTG.core.tagstore import Tag
from GTG.gtk.browser import GnomeConfig, tasktree, tagtree
-#from GTG.taskbrowser.preferences import PreferencesDialog
from GTG.gtk.browser.tasktree import TaskTreeModel,\
ActiveTaskTreeView,\
ClosedTaskTreeView
from GTG.gtk.browser.tagtree import TagTree
from GTG.tools import openurl
-from GTG.tools.dates import strtodate,\
- no_date,\
+from GTG.tools.dates import no_date,\
FuzzyDate, \
get_canonical_date
from GTG.tools.logger import Log
@@ -159,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")
@@ -429,7 +426,7 @@
### HELPER FUNCTIONS ########################################################
def open_preferences(self,widget):
- self.vmanager.show_preferences(self.priv)
+ self.vmanager.open_preferences(self.priv)
def quit(self,widget=None):
self.vmanager.close_browser()
=== modified file 'GTG/gtk/browser/tagtree.py'
--- GTG/gtk/browser/tagtree.py 2010-06-18 16:46:10 +0000
+++ GTG/gtk/browser/tagtree.py 2010-06-23 01:19:23 +0000
@@ -72,7 +72,8 @@
task = self.req.get_task(tid)
if task:
for tag in task.get_tags():
- self.tagrefresh(sender=sender,tagname=tag.get_name())
+ if tag:
+ self.tagrefresh(sender=sender,tagname=tag.get_name())
def tagrefresh(self,sender=None,tagname=None):
if tagname:
=== modified file 'GTG/gtk/crashhandler.py'
--- GTG/gtk/crashhandler.py 2010-06-07 21:14:45 +0000
+++ GTG/gtk/crashhandler.py 2010-06-23 01:19:23 +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/gtk/delete_dialog.py'
--- GTG/gtk/delete_dialog.py 2010-06-23 00:38:13 +0000
+++ GTG/gtk/delete_dialog.py 2010-06-23 01:19:23 +0000
@@ -43,7 +43,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/gtk/editor/editor.py'
--- GTG/gtk/editor/editor.py 2010-06-07 21:14:45 +0000
+++ GTG/gtk/editor/editor.py 2010-06-23 01:19:23 +0000
@@ -39,7 +39,7 @@
from GTG import _
from GTG import ngettext
from GTG import PLUGIN_DIR
-from GTG import DATA_DIR
+from GTG.core import CoreConfig
from GTG.gtk.editor import GnomeConfig
from GTG.gtk.editor.taskview import TaskView
from GTG.core.plugins.engine import PluginEngine
@@ -176,7 +176,7 @@
self.pengine = PluginEngine(PLUGIN_DIR)
self.te_plugin_api = PluginAPI(window = self.window,
config = None,
- data_dir = DATA_DIR,
+ data_dir = CoreConfig().get_data_dir(),
builder = self.builder,
requester = self.req,
tagpopup = None,
=== modified file 'GTG/gtk/manager.py'
--- GTG/gtk/manager.py 2010-06-10 14:45:36 +0000
+++ GTG/gtk/manager.py 2010-06-23 01:19:23 +0000
@@ -40,7 +40,11 @@
from GTG.core.plugins.api import PluginAPI
from GTG.tools.logger import Log
+
+
class Manager:
+
+
############## init #####################################################
def __init__(self, req, config):
self.config_obj = config
@@ -72,9 +76,9 @@
#Deletion UI
self.delete_dialog = None
- #Preferences windows
- # Initialize "Preferences" dialog
- self.preferences = None
+ #Preferences and Backends windows
+ # Initialize dialogs
+ self.preferences_dialog = None
#DBus
DBusTaskWrapper(self.req, self)
@@ -89,7 +93,7 @@
# initializes the plugin api class
self.plugin_api = PluginAPI(window = self.browser.window,
config = self.config,
- data_dir = GTG.DATA_DIR,
+ data_dir = self.config_obj.get_data_dir(),
builder = self.browser.builder,
requester = self.req,
tagpopup = self.browser.tagpopup,
@@ -189,8 +193,8 @@
################ Others dialog ############################################
- def show_preferences(self, config_priv, sender=None):
- if not self.preferences:
+ def open_preferences(self, config_priv, sender=None):
+ if not hasattr(self, "preferences"):
self.preferences = PreferencesDialog(self.pengine, self.p_apis, \
self.config_obj)
self.preferences.activate(config_priv)
=== modified file 'GTG/gtk/preferences.glade'
--- GTG/gtk/preferences.glade 2010-06-02 18:12:23 +0000
+++ GTG/gtk/preferences.glade 2010-06-23 01:19:23 +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 'GTG/gtk/preferences.py'
--- GTG/gtk/preferences.py 2010-06-10 14:45:36 +0000
+++ GTG/gtk/preferences.py 2010-06-23 01:19:23 +0000
@@ -270,7 +270,7 @@
self.config["plugins"]["enabled"] = \
self.pengine.enabled_plugins().keys()
- self.config_obj.save_config()
+ self.config_obj.save()
self.dialog.hide()
return True
=== modified file 'GTG/tests/__init__.py'
--- GTG/tests/__init__.py 2010-06-22 09:43:55 +0000
+++ GTG/tests/__init__.py 2010-06-23 01:19:23 +0000
@@ -19,22 +19,31 @@
"""Unit tests for GTG."""
+from GTG.tools.testingmode import TestingMode
+TestingMode().set_testing_mode(True)
+
+
import unittest
+
from GTG.tests import (
test_tagstore,
test_taskviewserial,
test_tree,
test_apidocs,
+ test_backends,
+ test_datastore,
test_filteredtree,
)
-
def test_suite():
return unittest.TestSuite([
test_tagstore.test_suite(),
test_taskviewserial.test_suite(),
test_tree.test_suite(),
test_apidocs.test_suite(),
+ test_backends.test_suite(),
+ test_datastore.test_suite(),
test_filteredtree.test_suite(),
])
+
=== modified file 'GTG/tests/test_apidocs.py'
--- GTG/tests/test_apidocs.py 2010-05-29 14:39:28 +0000
+++ GTG/tests/test_apidocs.py 2010-06-23 01:19:23 +0000
@@ -27,6 +27,8 @@
import shutil
import uuid
+from GTG.core import CoreConfig
+
class TestApiDocs(unittest.TestCase):
@@ -50,4 +52,6 @@
shutil.rmtree(api_dir)
def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
return unittest.TestLoader().loadTestsFromTestCase(TestApiDocs)
=== 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-06-23 01:19:23 +0000
@@ -0,0 +1,191 @@
+# -*- 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
+from GTG.core import CoreConfig
+
+
+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 SetUp(self):
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
+
+ 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'], "string")
+
+ 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["Enabled"] = True
+ 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():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
+ 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-06-23 01:19:23 +0000
@@ -0,0 +1,360 @@
+# -*- 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())
+ time.sleep(1)
+# #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():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
+ return unittest.TestLoader().loadTestsFromTestCase(TestDatastore)
+
+
+
+class FakeBackend(unittest.TestCase):
+ '''
+ 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 queue_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 queue_remove_task(self, task_id):
+ self.tasks_ids.remove(task_id)
+
+ def get_id(self):
+ return self.backend_id
+
+ def start_get_tasks(self):
+ for task_id in self.tasks_ids:
+ self.datastore.push_task(self.datastore.task_factory(task_id))
+
+ def quit(self, disabled = False):
+ self.enabled = not disabled
+
+ def purge(self):
+ self.purged = True
+
+ def is_default(self):
+ return True
+
+ def set_parameter(self, param_name, param_value):
+ pass
+
+ def get_attached_tags(self):
+ return [CoreConfig.ALLTASKS_TAG]
+
+ def register_datastore(self, datastore):
+ self.datastore = datastore
+
+ ##########################################################################
+ # 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
=== modified file 'GTG/tests/test_tagstore.py'
--- GTG/tests/test_tagstore.py 2010-05-29 12:50:20 +0000
+++ GTG/tests/test_tagstore.py 2010-06-23 01:19:23 +0000
@@ -23,6 +23,7 @@
from GTG.core.tagstore import Tag
from GTG.core.datastore import DataStore
+from GTG.core import CoreConfig
@@ -117,4 +118,6 @@
self.assertEqual(0, len(save_calls))
def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
return unittest.TestLoader().loadTestsFromTestCase(TestTag)
=== modified file 'GTG/tests/test_taskviewserial.py'
--- GTG/tests/test_taskviewserial.py 2010-06-11 14:30:12 +0000
+++ GTG/tests/test_taskviewserial.py 2010-06-23 01:19:23 +0000
@@ -27,6 +27,7 @@
import unittest
from GTG.gtk.editor import taskviewserial
+from GTG.core import CoreConfig
class GtgBackendsUniTests(unittest.TestCase):
"""Tests for GTG backends."""
@@ -47,4 +48,6 @@
def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
return unittest.TestLoader().loadTestsFromName(__name__)
=== modified file 'GTG/tests/test_tree.py'
--- GTG/tests/test_tree.py 2010-02-21 19:21:18 +0000
+++ GTG/tests/test_tree.py 2010-06-23 01:19:23 +0000
@@ -22,6 +22,9 @@
import unittest
from GTG.core.tree import Tree,TreeNode
+from GTG.core import CoreConfig
+
+
class TestTree(unittest.TestCase):
"""Tests for `Tree`."""
@@ -119,4 +122,6 @@
def test_suite():
+ CoreConfig().set_data_dir("./test_data")
+ CoreConfig().set_conf_dir("./test_data")
return unittest.TestLoader().loadTestsFromName(__name__)
=== added file 'GTG/tools/borg.py'
--- GTG/tools/borg.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/borg.py 2010-06-23 01:19:23 +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
+
+
=== added file 'GTG/tools/keyring.py'
--- GTG/tools/keyring.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/keyring.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,48 @@
+# -*- 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/>.
+# -----------------------------------------------------------------------------
+
+import gnomekeyring
+
+from GTG.tools.borg import Borg
+
+
+
+class Keyring(Borg):
+
+
+ def __init__(self):
+ super(Keyring, self).__init__()
+ if not hasattr(self, "keyring"):
+ self.keyring = gnomekeyring.get_default_keyring_sync()
+
+ def set_password(self, name, password, userid = ""):
+ return gnomekeyring.item_create_sync(
+ self.keyring,
+ gnomekeyring.ITEM_GENERIC_SECRET,
+ name,
+ {"backend": name},
+ password,
+ True)
+
+ def get_password(self, item_id):
+ try:
+ item_info = gnomekeyring.item_get_info_sync(self.keyring, item_id)
+ return item_info.get_secret()
+ except (gnomekeyring.DeniedError, gnomekeyring.NoMatchError):
+ return ""
=== modified file 'GTG/tools/logger.py'
--- GTG/tools/logger.py 2010-03-02 06:32:31 +0000
+++ GTG/tools/logger.py 2010-06-23 01:19:23 +0000
@@ -41,6 +41,7 @@
#Shouldn't be needed, but the following line makes sure that
# this is a Singleton.
self.__dict__['_Debug__logger'] = Debug.__logger
+ self.debugging_mode = False
def __init_logger(self):
Debug.__logger = logging.getLogger('gtg_logger')
@@ -60,5 +61,10 @@
""" Delegates to the real logger """
return setattr(Debug.__logger, attr, value)
+ def set_debugging_mode(self, value):
+ self.debugging_mode = value
+ def is_debugging_mode(self):
+ return self.debugging_mode
+
#The singleton itself
Log = Debug()
=== added file 'GTG/tools/synchronized.py'
--- GTG/tools/synchronized.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/synchronized.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,14 @@
+from __future__ import with_statement
+from threading import Lock
+
+def synchronized(fun):
+ the_lock = Lock()
+
+ def fwrap(function):
+ def newFunction(*args, **kw):
+ with the_lock:
+ return function(*args, **kw)
+
+ return newFunction
+
+ return fwrap(fun)
=== modified file 'GTG/tools/taskxml.py'
--- GTG/tools/taskxml.py 2010-06-18 16:36:17 +0000
+++ GTG/tools/taskxml.py 2010-06-23 01:19:23 +0000
@@ -60,7 +60,15 @@
cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
if "" in cur_tags: cur_tags.remove("")
for tag in cur_tags: cur_task.tag_added(saxutils.unescape(tag))
-
+
+ #REMOTE TASK IDS
+ remote_ids_list = xmlnode.getElementsByTagName("task-remote-ids")
+ for remote_id in remote_ids_list:
+ if remote_id.childNodes:
+ node = remote_id.childNodes[0]
+ backend_id = node.firstChild.nodeValue
+ remote_task_id = node.childNodes[1].firstChild.nodeValue
+ task.add_remote_id(backend_id, remote_task_id)
return cur_task
#Task as parameter the doc where to put the XML node
@@ -99,4 +107,18 @@
#t_xml.appendChild(element.firstChild)
cleanxml.addTextNode(doc,t_xml,"content",desc)
#self.__write_textnode(doc,t_xml,"content",t.get_text())
+
+ #REMOTE TASK IDS
+ remote_ids_element = doc.createElement("task-remote-ids")
+ t_xml.appendChild(remote_ids_element)
+ remote_ids_dict = task.get_remote_ids()
+ for backend_id, task_id in remote_ids_dict.iteritems():
+ backend_element = doc.createElement('backend')
+ remote_ids_element.appendChild(backend_element)
+ backend_element.appendChild(doc.createTextNode(backend_id))
+ task_element = doc.createElement('task-id')
+ backend_element.appendChild(task_element)
+ task_element.appendChild(doc.createTextNode(task_id))
+
+
return t_xml
=== added file 'GTG/tools/testingmode.py'
--- GTG/tools/testingmode.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/testingmode.py 2010-06-23 01:19:23 +0000
@@ -0,0 +1,16 @@
+from GTG.tools.borg import Borg
+
+
+
+class TestingMode(Borg):
+
+
+ def set_testing_mode(self, value):
+ self._testing_mode = value
+
+ def get_testing_mode(self):
+ try:
+ return self._testing_mode
+ except:
+ return False
+
=== modified file 'Makefile'
--- Makefile 2010-03-01 01:43:33 +0000
+++ Makefile 2010-06-23 01:19:23 +0000
@@ -35,7 +35,7 @@
# Check for coding standard violations & flakes.
lint: pyflakes pep8
-.PHONY: check lint pyflakes pep8 apidocs
+.PHONY: check lint pyflakes pep8 apidocs edit-apidocs clean
#Ignore the exit code in pyflakes, so that pep8 is always run when "make lint"
.IGNORE: pyflakes
=== modified file 'scripts/debug.sh'
--- scripts/debug.sh 2010-06-21 09:44:23 +0000
+++ scripts/debug.sh 2010-06-23 01:19:23 +0000
@@ -42,6 +42,7 @@
if [ $norun -eq 0 ]; then
if [ $profile -eq 1 ]; then
python -m cProfile -o gtg.prof ./gtg
+ python ./scripts/profile_interpret.sh
else
./gtg $args
fi
=== added file 'scripts/profile_interpret.sh'
--- scripts/profile_interpret.sh 1970-01-01 00:00:00 +0000
+++ scripts/profile_interpret.sh 2010-06-23 01:19:23 +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)