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.
_______________________________________________ Mailing list: https://launchpad.net/~gtg Post to : [email protected] Unsubscribe : https://launchpad.net/~gtg More help : https://help.launchpad.net/ListHelp

