gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #03409
[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.