← Back to team overview

gtg team mailing list archive

[Merge] lp:~gtg/gtg/downgrade-support into lp:gtg/0.2

 

Izidor Matušov has proposed merging lp:~gtg/gtg/downgrade-support into lp:gtg/0.2.

Requested reviews:
  Paulo Cabido (pcabido)
Related bugs:
  Bug #827658 in Getting Things GNOME!: "GTG stable no longer starts after trying trunk"
  https://bugs.launchpad.net/gtg/+bug/827658

For more details, see:
https://code.launchpad.net/~gtg/gtg/downgrade-support/+merge/83485

It adds the code for loading 0.2.9 files and downgrading them to 0.2.4 files. Users can try the newest version and afterwards return to their normal (stable) version of GTG.

This include:
  - changes to loading projects.xml
  - changes to localfile backend
  - standalone script for downgrading files
-- 
https://code.launchpad.net/~gtg/gtg/downgrade-support/+merge/83485
Your team Gtg developers is subscribed to branch lp:~gtg/gtg/downgrade-support.
=== modified file 'GTG/backends/localfile.py'
--- GTG/backends/localfile.py	2009-12-03 09:47:09 +0000
+++ GTG/backends/localfile.py	2011-11-26 19:28:26 +0000
@@ -27,6 +27,8 @@
 from GTG.core  import CoreConfig
 from GTG.tools import cleanxml, taskxml
 
+import xml.dom.minidom
+
 #Return the name of the backend as it should be displayed in the UI
 def get_name():
     return "Local File"
@@ -65,21 +67,62 @@
         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:
+        if os.path.exists(zefile):
+            self.zefile = zefile
+        else:
             self.zefile = os.path.join(CoreConfig.DATA_DIR, zefile)
-            self.filename = zefile
-        else:
-            self.zefile = zefile
-            self.filename = zefile
+        self.filename = os.path.basename(self.zefile)
         #Create the defaut tasks for the first run.
         #We write the XML object in a file
-        if firstrunxml and not os.path.exists(zefile):
+        if firstrunxml and not os.path.exists(self.zefile):
             #shutil.copy(firstrunfile,self.zefile)
             cleanxml.savexml(self.zefile, firstrunxml)
         self.doc, self.xmlproj = cleanxml.openxmlfile(self.zefile, "project")
 
+        # Do downgrade
+        self.pid = parameters['pid']
+        if "need_convert" in parameters:
+            if parameters["need_convert"]:
+                self.downgrade()
+            parameters.pop("need_convert")
+            cleanxml.savexml(self.zefile, self.doc, backup=True)
+
+    # Downgrade 0.2.9 format of backend_localfile
+    def downgrade(self):
+        # convert to old format of tasks
+        def convert_id(node_id):
+            if '@' in node_id:
+                node_id = node_id.split('@')[0]
+
+            return node_id + '@' + self.pid
+
+        for node in self.xmlproj.childNodes:
+            node_id = node.getAttribute("id")
+            node_id = convert_id(node_id)
+            node.setAttribute("id", node_id)
+
+            for sub_node in node.childNodes:
+                if sub_node.tagName == "subtask":
+                    subtask = sub_node.firstChild.nodeValue
+                    subtask = convert_id(subtask)
+                    sub_node.firstChild.nodeValue = subtask
+                elif sub_node.tagName == "content":
+                    tas = "<content>%s</content>" % sub_node.firstChild.nodeValue
+                    content = xml.dom.minidom.parseString(tas)
+                    for subtask_node in content.getElementsByTagName('subtask'):
+                        subtask_id = subtask_node.firstChild.nodeValue
+                        subtask_id = convert_id(subtask_id)
+                        subtask_node.firstChild.nodeValue = subtask_id
+
+                    # Convert back into string
+                    str_content = content.toxml()
+                    str_content = str_content.partition("<content>")[2]
+                    str_content = str_content.partition("</content>")[0]
+
+                    sub_node.firstChild.nodeValue = str_content
+
     #Return the list of the task ID available in this backend
     def get_tasks_list(self):
         #time.sleep(2)

=== modified file 'GTG/core/__init__.py'
--- GTG/core/__init__.py	2009-12-28 15:30:05 +0000
+++ GTG/core/__init__.py	2011-11-26 19:28:26 +0000
@@ -115,6 +115,13 @@
             else:
                 dic["module"] = "localfile"
                 dic["pid"] = str(pid)
+
+            # Downgrade module name from 0.2.9
+            if dic["module"] == "backend_localfile":
+                dic["module"] = "localfile"
+                dic["need_convert"] = True
+            else:
+                dic["need_convert"] = False
             
             dic["xmlobject"] = xp
             pid += 1
@@ -125,6 +132,7 @@
         if len(backend_fn) == 0:
             dic = {}
             dic["module"] = "localfile"
+            dic["need_convert"] = False
             dic["pid"] = "1"
             backend_fn.append(dic)
             firstrun = True
@@ -135,7 +143,15 @@
             #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)
+            try:
+                module   = __import__(module_name)
+            except ImportError:
+                # Ignore import problems of 0.2.9 backends and skip them
+                if b["module"].startswith('backend_'):
+                    continue
+                else:
+                    raise
+
             module   = getattr(module, "backends")
             classobj = getattr(module, b["module"])
             b["parameters"] = classobj.get_parameters()
@@ -146,6 +162,12 @@
                 for key in b["parameters"]:
                     if xp.hasAttribute(key):
                         b[key] = str(xp.getAttribute(key))
+
+                if b["need_convert"] and "filename" not in b:
+                    key = "path"
+                    if xp.hasAttribute(key):
+                        b["filename"] = str(xp.getAttribute(key))
+                    
             if firstrun:
                 frx = firstrun_tasks.populate()
                 back = classobj.Backend(b,firstrunxml=frx)
@@ -153,6 +175,9 @@
                 back = classobj.Backend(b)
             #We put the backend itself in the dic
             b["backend"] = back
+
+        # Remove skipped backends
+        backend_fn = [backend for backend in backend_fn if "backend" in backend]
             
         return backend_fn
         

=== added file 'downgrade-gtg-0.2.9.py'
--- downgrade-gtg-0.2.9.py	1970-01-01 00:00:00 +0000
+++ downgrade-gtg-0.2.9.py	2011-11-26 19:28:26 +0000
@@ -0,0 +1,198 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2011 - Lionel Dricot & Bertrand Rousseau & Izidor Matušov
+#
+# 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 os
+import xml.dom.minidom
+import shutil
+import sys
+
+# Where data are stored
+from xdg.BaseDirectory import xdg_data_home
+data_home = os.path.join(xdg_data_home, 'gtg')
+
+##### Code from clean.xml #####################################################
+#This is for the awful pretty xml things
+tab = "\t"
+enter = "\n"
+BACKUP_NBR = 7
+
+#Those two functions are there only to be able to read prettyXML
+#Source : http://yumenokaze.free.fr/?/Informatique/Snipplet/Python/cleandom       
+def cleanDoc(document, indent="", newl=""):
+    node = document.documentElement
+    cleanNode(node, indent, newl)
+
+def cleanNode(currentNode, indent, newl):
+    myfilter = indent + newl
+    if currentNode.hasChildNodes:
+        for node in currentNode.childNodes:
+            if node.nodeType == 3 :
+                node.nodeValue = node.nodeValue.lstrip(myfilter).strip(myfilter)
+                if node.nodeValue == "":
+                    currentNode.removeChild(node)
+        for node in currentNode.childNodes:
+            cleanNode(node, indent, newl)
+
+#This function open an XML file if it exists and return the XML object
+#If the file doesn't exist, it is created with an empty XML tree    
+def openxmlfile(zefile):
+    try :
+        doc = xml.dom.minidom.parse(zefile)
+        cleanDoc(doc, tab, enter)
+        return doc
+    except IOError, msg:
+        print msg
+        sys.exit(1)
+    except xml.parsers.expat.ExpatError, msg:
+        print "Error parsing XML file %s: %s" %(zefile, msg)
+        sys.exit(1)
+
+#write a XML doc to a file
+def savexml(zefile, doc, backup=False) :
+    f = open(zefile, mode='w+')
+    pretty = doc.toprettyxml(tab, enter)
+    if f and pretty:
+        f.write(pretty.encode("utf-8"))
+        f.close()
+        if backup :
+            #We will now backup the file
+            backup_nbr = BACKUP_NBR
+            #We keep BACKUP_NBR versions of the file
+            #The 0 is the youngest one
+            while backup_nbr > 0 :
+                older = "%s.bak.%s" %(zefile,backup_nbr)
+                backup_nbr -= 1
+                newer = "%s.bak.%s" %(zefile,backup_nbr)
+                if os.path.exists(newer) :
+                    shutil.move(newer,older)
+            #The bak.0 is always a fresh copy of the closed file
+            #So that it's not touched in case of bad opening next time
+            current = "%s.bak.0" %(zefile)
+            shutil.copy(zefile,current)
+    else:
+        print "no file %s or no pretty xml" % zefile
+
+############################ Code for updating ################################
+
+def update_projects():
+    """ Update projects file:
+
+      - rename backend_localfile into localfile, create attribute filename
+            instead of path, return as need for convertion
+      - remove backends starting with "backend_" (they come from 0.2.9)
+    """
+
+    # Open projects.xml file
+    zefile = os.path.join(data_home, 'projects.xml')
+    doc = openxmlfile(zefile)
+    xmlproject = doc.getElementsByTagName("backend")
+
+    to_remove = []
+    need_convert = []
+    for xp in xmlproject:
+        # load module type
+        if xp.hasAttribute("module"):
+            module = str(xp.getAttribute("module"))
+            pid = str(xp.getAttribute("pid"))
+        else:
+            module = "localfile"
+            xp.setAttribute("module", module)
+
+        # convert localfile
+        if module == "backend_localfile":
+            module = "localfile"
+            xp.setAttribute("module", module)
+
+            if xp.hasAttribute("filename"):
+                filename = str(xp.getAttribute("filename"))
+            elif xp.hasAttribute("path"):
+                filename = str(xp.getAttribute("path"))
+                xp.setAttribute("filename", filename)
+                xp.removeAttribute("path")
+
+            need_convert.append((filename, pid))
+
+        # Remove 0.2.9 backends
+        if module.startswith("backend_"):
+            to_remove.append(xp)
+
+    # Remove unusued backends
+    for child in to_remove:
+        xmlproject.removeChild(child)
+
+    # Save it
+    savexml(zefile, doc, backup=True)
+
+    # Return localfile backends which must be downgraded
+    return need_convert
+
+def convert_id(node_id, pid):
+    """ GTG 0.2.4 expects to have have ids like uid@pid.
+    This function ensures, that id has the correct pid. """
+    if '@' in node_id:
+        node_id = node_id.split('@')[0]
+
+    return node_id + '@' + pid
+
+def downgrade_localfile(filename, pid):
+    """ Every id need to be updated, i.e.:
+       - task id
+       - subtask id
+       - subtask id in content
+    """
+    doc = openxmlfile(filename)
+    project = doc.documentElement
+
+    for node in project.childNodes:
+        node_id = node.getAttribute("id")
+        node_id = convert_id(node_id, pid)
+        node.setAttribute("id", node_id)
+
+        for sub_node in node.childNodes:
+            if sub_node.tagName == "subtask":
+                subtask = sub_node.firstChild.nodeValue
+                subtask = convert_id(subtask, pid)
+                sub_node.firstChild.nodeValue = subtask
+
+            elif sub_node.tagName == "content":
+                tas = "<content>%s</content>" % sub_node.firstChild.nodeValue
+                tas = unicode(tas).encode('UTF-8')
+                content = xml.dom.minidom.parseString(tas)
+
+                for subtask_node in content.getElementsByTagName('subtask'):
+                    subtask_id = subtask_node.firstChild.nodeValue
+                    subtask_id = convert_id(subtask_id, pid)
+                    subtask_node.firstChild.nodeValue = subtask_id
+
+                # Convert back into string
+                str_content = content.toxml()
+                str_content = str_content.partition("<content>")[2]
+                str_content = str_content.partition("</content>")[0]
+
+                sub_node.firstChild.nodeValue = str_content
+
+    # Save the work
+    savexml(filename, doc, backup=True)
+
+if __name__ == "__main__":
+    need_convert = update_projects()
+    for filename, pid in need_convert:
+        print "Downgrading", filename
+        downgrade_localfile(filename, pid)


Follow ups