← Back to team overview

gtg team mailing list archive

[Merge] lp:~essr1/gtg/desktop-couch-toproposed into lp:gtg

 

Shenja Sosna has proposed merging lp:~essr1/gtg/desktop-couch-toproposed into lp:gtg.

Requested reviews:
  Gtg developers (gtg)

For more details, see:
https://code.launchpad.net/~essr1/gtg/desktop-couch-toproposed/+merge/75178

add couchdb backend. 

I'm just learning, tell me if poorly written
-- 
https://code.launchpad.net/~essr1/gtg/desktop-couch-toproposed/+merge/75178
Your team Gtg developers is requested to review the proposed merge of lp:~essr1/gtg/desktop-couch-toproposed into lp:gtg.
=== added file 'GTG/backends/backend_couchdb.py'
--- GTG/backends/backend_couchdb.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_couchdb.py	2011-09-13 13:34:25 +0000
@@ -0,0 +1,488 @@
+# -*- 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.
+
+This backend contains comments that are meant as a reference, in case someone
+wants to write a backend.
+'''
+
+import os
+#import uuid
+
+from desktopcouch.records.server import CouchDatabase
+from desktopcouch.records.record import Record as CouchRecord
+from pprint import pprint
+import xml.sax.saxutils as saxutils
+
+import datetime
+
+from GTG.backends.genericbackend        import GenericBackend
+from GTG                                import _
+from GTG.backends.syncengine            import SyncEngine, SyncMeme
+from GTG.backends.periodicimportbackend import PeriodicImportBackend
+#from GTG.tools.dates                    import RealDate, NoDate
+from GTG.tools                          import dates
+#from GTG.core.task                      import Task
+from GTG.tools.interruptible            import interruptible
+from GTG.tools.liblarch.tree            import TreeNode
+#from GTG.tools.liblarch                 import Tree
+#from GTG.tools.tags                     import extract_tags_from_text
+
+#from GTG.core                           import CoreConfig
+from GTG.tools.logger                   import Log
+
+RECORD_TYPE = "http://live.gnome.org/gtg/couchdb/task";
+
+
+class Backend(PeriodicImportBackend):
+    '''
+    Localfile backend, which stores your tasks in a XML file in the standard
+    XDG_DATA_DIR/gtg folder (the path is configurable).
+    An instance of this class is used as the default backend for GTG.
+    This backend loads all the tasks stored in the localfile after it's enabled,
+    and from that point on just writes the changes to the file: it does not
+    listen for eventual file changes
+    '''
+    #General description of the backend: these are used to show a description of
+    # the backend to the user when s/he is considering adding it.
+    # BACKEND_NAME is the name of the backend used internally (it must be
+    # unique).
+    #Please note that BACKEND_NAME and BACKEND_ICON_NAME should *not* be
+    #translated.
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_couchdb", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("CouchDB"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Ryan Paul", \
+                "Bryce Harrington", "Jeff Craig", "Senja Sosna"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("Your tasks are saved in a Couch DB (Couch database). " + \
+              " This is the most basic and the default way " +   \
+              "for GTG to save your tasks."),\
+        }
+
+    #These are the parameters to configure a new backend of this type. A
+    # parameter has a name, a type and a default value.
+    # Here, we define a parameter "path", which is a string, and has a default
+    # value as a random file in the default path
+    #NOTE: to keep this simple, the filename default path is the same until GTG
+    #      is restarted. I consider this a minor annoyance, and we can avoid
+    #      coding the change of the path each time a backend is
+    #      created (invernizzi)
+    _static_parameters = { \
+        "dbname": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
+            GenericBackend.PARAM_DEFAULT_VALUE: "gtgtasks", }, \
+        "period": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
+            GenericBackend.PARAM_DEFAULT_VALUE: 1, },
+        "is-first-run": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
+            GenericBackend.PARAM_DEFAULT_VALUE: True, },
+            }
+
+    def __init__(self, parameters):
+        """
+        Instantiates a new backend.
+
+        @param parameters: A dictionary of parameters, generated from
+        _static_parameters. A few parameters are added to those, the list of
+        these is in the "DefaultBackend" class, look for the KEY_* constants.
+    
+        The backend should take care if one expected value is None or
+        does not exist in the dictionary.
+        """
+        super(Backend, self).__init__(parameters)
+        self.tids = [] #we keep the list of loaded task ids here
+        
+        #self.database = CouchDatabase(self._parameters["dbname"], \
+        #                                 create=True)
+        self.sync_engine_path = os.path.join('backends/couchdb/', \
+                                      "sync_engine-" + self.get_id())
+        self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
+                                                   SyncEngine())
+        
+
+    def initialize(self):
+        """This is called when a backend is enabled"""
+        super(Backend, self).initialize()
+        Log.debug('db name is %s'%self._parameters["dbname"])
+        self.database = CouchDatabase(self._parameters["dbname"], create=True)
+
+    def get_tasks_list(self):
+        result = self.database.get_records(RECORD_TYPE, True)
+        toreturn = []
+        for r in result:
+            value = r.value
+            toreturn.append(value['_id'])
+        return toreturn
+    def get_tasks_sorted(self):
+        ''' return all tasks by level sorted
+        '''
+        list_to_sort = []
+        for tid in self.datastore.get_all_tasks():
+            gtg_task = self.datastore.get_task(tid)
+            tree = gtg_task.get_tree() 
+            patch = tree.get_paths_for_node(tid)
+            level = reduce(max, map(len, path))
+            list_to_sort.append((level, tid))
+        list_to_sort.sort() 
+        list_to_return = [r[1] for r in reversed(list_to_sort)]
+        return list_to_return
+
+    def do_periodic_import(self):
+        """
+        See PeriodicImportBackend for an explanation of this function.
+        """
+        stored_couchdb_task_ids = set(self.sync_engine.get_all_remote())
+        current_couchdb_task_ids = set(self.get_tasks_list())
+        #If it's the very first time the backend is run, it's possible that the
+        # user already synced his tasks in some way (but we don't know that).
+        # Therefore, we attempt to induce those tasks relationships matching the
+        # titles.
+        if self._parameters["is-first-run"]:
+            gtg_titles_dic = {}
+            #for tid in self.datastore.get_all_tasks():
+            for tid in self.get_tasks_sorted():
+                gtg_task = self.datastore.get_task(tid)
+                if not self._gtg_task_is_syncable_per_attached_tags(gtg_task):
+                    continue
+                gtg_title = gtg_task.get_title()
+                if gtg_titles_dic.has_key(gtg_title):
+                    gtg_titles_dic[gtg_task.get_title()].append(tid)
+                else:
+                    gtg_titles_dic[gtg_task.get_title()] = [tid]
+            for couch_task_id in current_couchdb_task_ids:
+                couch_task = self._couch_get_task(couch_task_id)
+                try:
+                    tids = gtg_titles_dic[couch_task['title']]
+                    #we remove the tid, so that it can't be linked to two
+                    # different couch tasks
+                    if len(tids)==0: continue
+                    tid = tids.pop()
+                    gtg_task = self.datastore.get_task(tid)
+                    meme = SyncMeme(gtg_task.get_modified(),
+                                    self._couch_get_modified(couch_task),
+                                    "GTG")
+                    self.sync_engine.record_relationship( \
+                         local_id = tid,
+                         remote_id = couch_task.record_id,
+                         meme = meme)
+                except KeyError:
+                    pass
+            
+            #a first run has been completed successfully
+            self._parameters["is-first-run"] = False
+        for couch_task_id in current_couchdb_task_ids:
+            #Adding and updating
+            self.cancellation_point()
+            self._process_couch_task(couch_task_id)
+
+        for couch_task_id in stored_couchdb_task_ids.difference(\
+                                current_couchdb_task_ids):
+            #Removing the old ones
+            self.cancellation_point()
+            tid = self.sync_engine.get_local_id(couch_task_id)
+            self.datastore.request_task_deletion(tid)
+            try:
+                self.sync_engine.break_relationship(remote_id = \
+                                                    couch_task_id)
+            except KeyError:
+                pass
+
+    def save_state(self):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        self._store_pickled_file(self.sync_engine_path, self.sync_engine)
+
+    @interruptible
+    def remove_task(self, tid):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        try:
+            couch_task_id = self.sync_engine.get_remote_id(tid)
+            if self._couch_has_task(couch_task_id):
+                self.database.delete_record(couch_task_id)
+        except KeyError:
+            pass
+        try:
+            self.sync_engine.break_relationship(local_id = tid)
+        except:
+            pass
+
+    @interruptible
+    def set_task(self, task):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        tid = task.get_id()
+        is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
+        action, couch_task_id = self.sync_engine.analyze_local_id(
+                                tid,
+                                self.datastore.has_task,
+                                self._couch_has_task,
+                                is_syncable)
+        Log.debug(\
+        'GTG->CouchDatabase set task (%s, %s, id %s, uuid %s)' % (action, 
+        is_syncable, tid, task.get_uuid()))
+
+        if action == None:
+            return
+
+        if action == SyncEngine.ADD:
+            couch_task = CouchRecord(record_id = task.get_uuid(),\
+                                         record_type=RECORD_TYPE)
+            with self.datastore.get_backend_mutex():
+                #self._evolution_tasks.add_object(evo_task)
+                self._populate_couch_task(task, couch_task)
+                meme = SyncMeme(task.get_modified(),
+                                self._couch_get_modified(couch_task),
+                                "GTG")
+                self.sync_engine.record_relationship( \
+                    local_id = tid, remote_id = couch_task.record_id,\
+                    meme = meme)
+
+        elif action == SyncEngine.UPDATE:
+            with self.datastore.get_backend_mutex():
+                couch_task = self._couch_get_task(couch_task_id)
+                meme = self.sync_engine.get_meme_from_local_id(task.get_id())
+                newest = meme.which_is_newest(task.get_modified(),
+                                     self._couch_get_modified(couch_task))
+                if newest == "local":
+                    self._populate_couch_task(task, couch_task)
+                    meme.set_remote_last_modified( \
+                                self._couch_get_modified(couch_task))
+                    meme.set_local_last_modified(task.get_modified())
+                else:
+                    #we skip saving the state
+                    return
+
+        elif action == SyncEngine.REMOVE:
+            self.datastore.request_task_deletion(tid)
+            try:
+                self.sync_engine.break_relationship(local_id = tid)
+            except KeyError:
+                pass
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            couch_task = self._couch_get_task(couch_task_id)
+            self._exec_lost_syncability(tid, couch_task)
+        self.save_state()
+
+
+    def _couch_get_task(self, uuid):
+        if self.database.record_exists(uuid):
+            td = self.database.get_record(uuid)
+            return td
+
+    def _couch_has_task(self, uuid):
+        return self.database.record_exists(uuid)
+
+    def _process_couch_task(self, couch_task_id):
+        '''
+        Takes an evolution task id and carries out the necessary operations to
+        refresh the sync state
+        '''
+        self.cancellation_point()
+        couch_task = self._couch_get_task(couch_task_id)
+        is_syncable = self._couch_task_is_syncable(couch_task)
+        action, tid = self.sync_engine.analyze_remote_id( \
+                         couch_task_id,
+                         self.datastore.has_task,
+                         self._couch_has_task,
+                         is_syncable)
+        Log.debug('CouchDatabase->GTG set task (%s, %s, %s)'% (action, \
+            is_syncable, couch_task_id))
+
+        if action == SyncEngine.ADD:
+            with self.datastore.get_backend_mutex():
+                #tid = str(uuid.uuid4())
+                tid = str(couch_task.record_id)
+                task = self.datastore.task_factory(tid)
+                self._populate_task(task, couch_task)
+                meme = SyncMeme(task.get_modified(),
+                                self._couch_get_modified(couch_task),
+                                "GTG")
+                self.sync_engine.record_relationship(local_id = tid,
+                                                     remote_id = couch_task_id,
+                                                     meme = meme)
+                self.datastore.push_task(task)
+
+        elif action == SyncEngine.UPDATE:
+            with self.datastore.get_backend_mutex():
+                task = self.datastore.get_task(tid)
+                meme = self.sync_engine.get_meme_from_remote_id(couch_task_id)
+                newest = meme.which_is_newest(task.get_modified(),
+                                    self._couch_get_modified(couch_task))
+                if newest == "remote":
+                    self._populate_task(task, couch_task)
+                    meme.set_remote_last_modified( \
+                            self._couch_get_modified(couch_task))
+                    meme.set_local_last_modified(task.get_modified())
+
+        elif action == SyncEngine.REMOVE:
+            return
+            try:
+                if self._couch_has_task(couch_task_id):
+                    self.database.delete_record(couch_task_id)
+                    self.sync_engine.break_relationship(remote_id = couch_task_id)
+            except KeyError:
+                pass
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            self._exec_lost_syncability(tid, couch_task)
+        self.save_state()
+
+    def _populate_task(self, task, couch_task):
+        '''
+        Updates the attributes of a GTG task copying the ones of an Evolution
+        task
+        '''
+        task.set_title(couch_task['title'])
+        text = couch_task['text']
+        if text == None:
+            text = ""
+        task.set_text(text)
+        task.set_uuid(couch_task.record_id)
+        cur_stat = "%s" %couch_task["status"]
+        donedate = couch_task["donedate"]
+        task.set_status(cur_stat,donedate=dates.strtodate(donedate))
+        task.set_due_date(dates.strtodate(couch_task["duedate"]))
+        task.set_start_date(dates.strtodate(couch_task["startdate"]))
+        cur_tags = couch_task["tags"].replace(' ','').split(",")
+        if "" in cur_tags: cur_tags.remove("")
+        for tag in cur_tags: task.tag_added(saxutils.unescape(tag))
+        if couch_task.get('childs')!=None:
+            child_ids = couch_task['childs']
+            local_synced_ids = []
+            for tid in child_ids:
+                try:
+                    local_id = self.sync_engine.get_local_id(tid)
+                except KeyError:
+                    pass
+                local_synced_ids.append(local_id)
+            tree = task.get_tree()
+            tree_node = TreeNode(task.get_id())
+            tree_node.set_tree(tree)
+            local_ids = set([t for t in tree_node.get_children()])
+            remote_ids = set([t for t in local_synced_ids])
+            for tid in local_ids.difference(remote_ids):
+                tree_node.remove_child(tid)
+            for tid in remote_ids.difference(local_ids):
+                tree_node.add_child(tid)
+        if couch_task.get('parent') !=None:
+            parent_ids = couch_task['parent']
+            local_synced_ids = []
+            for tid in parent_ids:
+                try:
+                    local_id = self.sync_engine.get_local_id(tid)
+                except KeyError:
+                    continue
+                local_synced_ids.append(local_id)
+            tree = task.get_tree()
+            tree_node = TreeNode(task.get_id())
+            tree_node.set_tree(tree)
+            local_ids =set([t for t in tree_node.get_parents()])
+            remote_ids = set([t for t in local_synced_ids])
+            for tid in local_ids.difference(remote_ids):
+                tree_node.remove_parent(tid)
+            for tid in remote_ids.difference(local_ids):
+                tree_node.add_parent(tid)
+
+    def _populate_couch_task(self, task, couch_task):
+        couch_task['title'] = task.get_title()
+        couch_task['status'] = task.get_status()
+        couch_task['donedate'] = task.get_due_date().xml_str()
+        tags_str = ""
+        for tag in task.get_tags_name(): 
+            tags_str = tags_str + saxutils.escape(str(tag)) + ","
+        couch_task['tags'] = tags_str[:-1]
+        couch_task["duedate"] = task.get_due_date().xml_str()
+        couch_task["modified"] = task.get_modified_string()
+        couch_task["startdate"] = task.get_start_date().xml_str()
+        couch_task["donedate"] = task.get_closed_date().xml_str()
+        text = task.get_excerpt(strip_tags = True, strip_subtasks = True)
+        couch_task['text'] = text
+        #treetask = TreeNode(task.get_id())
+        if task.has_parent():
+            parents = task.get_parents()
+            remote_ids = []
+            for p in parents:
+                try:
+                    remote_id = self.sync_engine.get_remote_id(p)
+                    remote_ids.append(remote_id)
+                except KeyError:
+                    pass
+            couch_task['parent'] = remote_ids
+        childrens = []  
+        for tid in task.get_children():
+            try:
+                remote_id = self.sync_engine.get_remote_id(tid)
+            except KeyError:
+                pass
+            childrens.append(remote_id)
+        couch_task['childrens'] = set([t for t in childrens])
+        if self._couch_has_task(couch_task.record_id):
+            self.database.update_fields(couch_task.record_id, couch_task)
+        else:
+            self.database.put_record(couch_task)
+
+    def _exec_lost_syncability(self, tid, couch_task):
+        '''
+        Executed when a relationship between tasks loses its syncability
+        property. See SyncEngine for an explanation of that.
+        This function finds out which object is the original one
+        and which is the copy, and deletes the copy.
+        '''
+        Log.debug('_exec_lost_syncability')
+        meme = self.sync_engine.get_meme_from_local_id(tid)
+        self.sync_engine.break_relationship(local_id = tid)
+        if meme.get_origin() == "GTG":
+            self.database.delete_record(couch_task.record_id)
+        else:
+            self.datastore.request_task_deletion(tid)
+
+    def _couch_task_is_syncable(self, couch_task):
+        '''
+        Returns True if this CouchDB task should be synced into GTG tasks.
+
+        @param couch_task: an couch task
+        @returns Boolean
+        '''
+        attached_tags = self.get_attached_tags()
+        if GenericBackend.ALLTASKS_TAG in attached_tags:
+            return True
+        cur_tags = couch_task["tags"].replace(' ','').split(",")
+        if "" in cur_tags: cur_tags.remove("")
+        for tag in cur_tags:
+            if "@" + saxutils.unescape(tag) in attached_tags:
+                return  True
+        return False
+
+    def _couch_get_modified(self, couch_task):
+        '''Returns the modified time of an Evolution task'''
+        modified_datetime = datetime.datetime.strptime(couch_task['modified'],\
+                                                    "%Y-%m-%dT%H:%M:%S")
+        return modified_datetime
+

=== modified file 'GTG/gtk/backends_dialog/parameters_ui/__init__.py'
--- GTG/gtk/backends_dialog/parameters_ui/__init__.py	2011-01-03 02:57:55 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/__init__.py	2011-09-13 13:34:25 +0000
@@ -100,6 +100,10 @@
                                        "targeted by the bug"), \
                              "parameter": "tag-with-project-name"}) \
                ),\
+               ("dbname", self.UI_generator(TextUI, \
+                            {"description": _("DataBase name"),
+                             "parameter_name": "dbname"})
+               ),\
             ) 
     def UI_generator(self, param_type, special_arguments = {}):
         '''A helper function to build a widget type from a template.