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

Proposed by Izidor Matušov on 2011-11-26
Status: Merged
Merged at revision: 696
Proposed branch: lp:~gtg/gtg/downgrade-support
Merge into: lp:gtg/0.2
Diff against target: 351 lines (+274/-8)
3 files modified
GTG/backends/localfile.py (+50/-7)
GTG/core/__init__.py (+26/-1)
downgrade-gtg-0.2.9.py (+198/-0)
To merge this branch: bzr merge lp:~gtg/gtg/downgrade-support
Reviewer Review Type Date Requested Status
Paulo Cabido 2011-11-26 Approve on 2011-11-27
Review via email: mp+83485@code.launchpad.net

Description of the change

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

To post a comment you must log in.
Paulo Cabido (pcabido) wrote :

All the code seams OK to me + I tested it and it worked well, no issues encountered. It's OK to merge.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'GTG/backends/localfile.py'
2--- GTG/backends/localfile.py 2009-12-03 09:47:09 +0000
3+++ GTG/backends/localfile.py 2011-11-26 19:28:26 +0000
4@@ -27,6 +27,8 @@
5 from GTG.core import CoreConfig
6 from GTG.tools import cleanxml, taskxml
7
8+import xml.dom.minidom
9+
10 #Return the name of the backend as it should be displayed in the UI
11 def get_name():
12 return "Local File"
13@@ -65,21 +67,62 @@
14 else:
15 zefile = "%s.xml" %(uuid.uuid4())
16 parameters["filename"] = zefile
17+
18 #For the day we want to open files somewhere else
19- default_folder = True
20- if default_folder:
21+ if os.path.exists(zefile):
22+ self.zefile = zefile
23+ else:
24 self.zefile = os.path.join(CoreConfig.DATA_DIR, zefile)
25- self.filename = zefile
26- else:
27- self.zefile = zefile
28- self.filename = zefile
29+ self.filename = os.path.basename(self.zefile)
30 #Create the defaut tasks for the first run.
31 #We write the XML object in a file
32- if firstrunxml and not os.path.exists(zefile):
33+ if firstrunxml and not os.path.exists(self.zefile):
34 #shutil.copy(firstrunfile,self.zefile)
35 cleanxml.savexml(self.zefile, firstrunxml)
36 self.doc, self.xmlproj = cleanxml.openxmlfile(self.zefile, "project")
37
38+ # Do downgrade
39+ self.pid = parameters['pid']
40+ if "need_convert" in parameters:
41+ if parameters["need_convert"]:
42+ self.downgrade()
43+ parameters.pop("need_convert")
44+ cleanxml.savexml(self.zefile, self.doc, backup=True)
45+
46+ # Downgrade 0.2.9 format of backend_localfile
47+ def downgrade(self):
48+ # convert to old format of tasks
49+ def convert_id(node_id):
50+ if '@' in node_id:
51+ node_id = node_id.split('@')[0]
52+
53+ return node_id + '@' + self.pid
54+
55+ for node in self.xmlproj.childNodes:
56+ node_id = node.getAttribute("id")
57+ node_id = convert_id(node_id)
58+ node.setAttribute("id", node_id)
59+
60+ for sub_node in node.childNodes:
61+ if sub_node.tagName == "subtask":
62+ subtask = sub_node.firstChild.nodeValue
63+ subtask = convert_id(subtask)
64+ sub_node.firstChild.nodeValue = subtask
65+ elif sub_node.tagName == "content":
66+ tas = "<content>%s</content>" % sub_node.firstChild.nodeValue
67+ content = xml.dom.minidom.parseString(tas)
68+ for subtask_node in content.getElementsByTagName('subtask'):
69+ subtask_id = subtask_node.firstChild.nodeValue
70+ subtask_id = convert_id(subtask_id)
71+ subtask_node.firstChild.nodeValue = subtask_id
72+
73+ # Convert back into string
74+ str_content = content.toxml()
75+ str_content = str_content.partition("<content>")[2]
76+ str_content = str_content.partition("</content>")[0]
77+
78+ sub_node.firstChild.nodeValue = str_content
79+
80 #Return the list of the task ID available in this backend
81 def get_tasks_list(self):
82 #time.sleep(2)
83
84=== modified file 'GTG/core/__init__.py'
85--- GTG/core/__init__.py 2009-12-28 15:30:05 +0000
86+++ GTG/core/__init__.py 2011-11-26 19:28:26 +0000
87@@ -115,6 +115,13 @@
88 else:
89 dic["module"] = "localfile"
90 dic["pid"] = str(pid)
91+
92+ # Downgrade module name from 0.2.9
93+ if dic["module"] == "backend_localfile":
94+ dic["module"] = "localfile"
95+ dic["need_convert"] = True
96+ else:
97+ dic["need_convert"] = False
98
99 dic["xmlobject"] = xp
100 pid += 1
101@@ -125,6 +132,7 @@
102 if len(backend_fn) == 0:
103 dic = {}
104 dic["module"] = "localfile"
105+ dic["need_convert"] = False
106 dic["pid"] = "1"
107 backend_fn.append(dic)
108 firstrun = True
109@@ -135,7 +143,15 @@
110 #We dynamically import modules needed
111 module_name = "GTG.backends.%s"%b["module"]
112 #FIXME : we should throw an error if the backend is not importable
113- module = __import__(module_name)
114+ try:
115+ module = __import__(module_name)
116+ except ImportError:
117+ # Ignore import problems of 0.2.9 backends and skip them
118+ if b["module"].startswith('backend_'):
119+ continue
120+ else:
121+ raise
122+
123 module = getattr(module, "backends")
124 classobj = getattr(module, b["module"])
125 b["parameters"] = classobj.get_parameters()
126@@ -146,6 +162,12 @@
127 for key in b["parameters"]:
128 if xp.hasAttribute(key):
129 b[key] = str(xp.getAttribute(key))
130+
131+ if b["need_convert"] and "filename" not in b:
132+ key = "path"
133+ if xp.hasAttribute(key):
134+ b["filename"] = str(xp.getAttribute(key))
135+
136 if firstrun:
137 frx = firstrun_tasks.populate()
138 back = classobj.Backend(b,firstrunxml=frx)
139@@ -153,6 +175,9 @@
140 back = classobj.Backend(b)
141 #We put the backend itself in the dic
142 b["backend"] = back
143+
144+ # Remove skipped backends
145+ backend_fn = [backend for backend in backend_fn if "backend" in backend]
146
147 return backend_fn
148
149
150=== added file 'downgrade-gtg-0.2.9.py'
151--- downgrade-gtg-0.2.9.py 1970-01-01 00:00:00 +0000
152+++ downgrade-gtg-0.2.9.py 2011-11-26 19:28:26 +0000
153@@ -0,0 +1,198 @@
154+#!/usr/bin/python
155+# -*- coding: utf-8 -*-
156+# -----------------------------------------------------------------------------
157+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
158+# Copyright (c) 2008-2011 - Lionel Dricot & Bertrand Rousseau & Izidor Matušov
159+#
160+# This program is free software: you can redistribute it and/or modify it under
161+# the terms of the GNU General Public License as published by the Free Software
162+# Foundation, either version 3 of the License, or (at your option) any later
163+# version.
164+#
165+# This program is distributed in the hope that it will be useful, but WITHOUT
166+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
167+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
168+# details.
169+#
170+# You should have received a copy of the GNU General Public License along with
171+# this program. If not, see <http://www.gnu.org/licenses/>.
172+# -----------------------------------------------------------------------------
173+
174+import os
175+import xml.dom.minidom
176+import shutil
177+import sys
178+
179+# Where data are stored
180+from xdg.BaseDirectory import xdg_data_home
181+data_home = os.path.join(xdg_data_home, 'gtg')
182+
183+##### Code from clean.xml #####################################################
184+#This is for the awful pretty xml things
185+tab = "\t"
186+enter = "\n"
187+BACKUP_NBR = 7
188+
189+#Those two functions are there only to be able to read prettyXML
190+#Source : http://yumenokaze.free.fr/?/Informatique/Snipplet/Python/cleandom
191+def cleanDoc(document, indent="", newl=""):
192+ node = document.documentElement
193+ cleanNode(node, indent, newl)
194+
195+def cleanNode(currentNode, indent, newl):
196+ myfilter = indent + newl
197+ if currentNode.hasChildNodes:
198+ for node in currentNode.childNodes:
199+ if node.nodeType == 3 :
200+ node.nodeValue = node.nodeValue.lstrip(myfilter).strip(myfilter)
201+ if node.nodeValue == "":
202+ currentNode.removeChild(node)
203+ for node in currentNode.childNodes:
204+ cleanNode(node, indent, newl)
205+
206+#This function open an XML file if it exists and return the XML object
207+#If the file doesn't exist, it is created with an empty XML tree
208+def openxmlfile(zefile):
209+ try :
210+ doc = xml.dom.minidom.parse(zefile)
211+ cleanDoc(doc, tab, enter)
212+ return doc
213+ except IOError, msg:
214+ print msg
215+ sys.exit(1)
216+ except xml.parsers.expat.ExpatError, msg:
217+ print "Error parsing XML file %s: %s" %(zefile, msg)
218+ sys.exit(1)
219+
220+#write a XML doc to a file
221+def savexml(zefile, doc, backup=False) :
222+ f = open(zefile, mode='w+')
223+ pretty = doc.toprettyxml(tab, enter)
224+ if f and pretty:
225+ f.write(pretty.encode("utf-8"))
226+ f.close()
227+ if backup :
228+ #We will now backup the file
229+ backup_nbr = BACKUP_NBR
230+ #We keep BACKUP_NBR versions of the file
231+ #The 0 is the youngest one
232+ while backup_nbr > 0 :
233+ older = "%s.bak.%s" %(zefile,backup_nbr)
234+ backup_nbr -= 1
235+ newer = "%s.bak.%s" %(zefile,backup_nbr)
236+ if os.path.exists(newer) :
237+ shutil.move(newer,older)
238+ #The bak.0 is always a fresh copy of the closed file
239+ #So that it's not touched in case of bad opening next time
240+ current = "%s.bak.0" %(zefile)
241+ shutil.copy(zefile,current)
242+ else:
243+ print "no file %s or no pretty xml" % zefile
244+
245+############################ Code for updating ################################
246+
247+def update_projects():
248+ """ Update projects file:
249+
250+ - rename backend_localfile into localfile, create attribute filename
251+ instead of path, return as need for convertion
252+ - remove backends starting with "backend_" (they come from 0.2.9)
253+ """
254+
255+ # Open projects.xml file
256+ zefile = os.path.join(data_home, 'projects.xml')
257+ doc = openxmlfile(zefile)
258+ xmlproject = doc.getElementsByTagName("backend")
259+
260+ to_remove = []
261+ need_convert = []
262+ for xp in xmlproject:
263+ # load module type
264+ if xp.hasAttribute("module"):
265+ module = str(xp.getAttribute("module"))
266+ pid = str(xp.getAttribute("pid"))
267+ else:
268+ module = "localfile"
269+ xp.setAttribute("module", module)
270+
271+ # convert localfile
272+ if module == "backend_localfile":
273+ module = "localfile"
274+ xp.setAttribute("module", module)
275+
276+ if xp.hasAttribute("filename"):
277+ filename = str(xp.getAttribute("filename"))
278+ elif xp.hasAttribute("path"):
279+ filename = str(xp.getAttribute("path"))
280+ xp.setAttribute("filename", filename)
281+ xp.removeAttribute("path")
282+
283+ need_convert.append((filename, pid))
284+
285+ # Remove 0.2.9 backends
286+ if module.startswith("backend_"):
287+ to_remove.append(xp)
288+
289+ # Remove unusued backends
290+ for child in to_remove:
291+ xmlproject.removeChild(child)
292+
293+ # Save it
294+ savexml(zefile, doc, backup=True)
295+
296+ # Return localfile backends which must be downgraded
297+ return need_convert
298+
299+def convert_id(node_id, pid):
300+ """ GTG 0.2.4 expects to have have ids like uid@pid.
301+ This function ensures, that id has the correct pid. """
302+ if '@' in node_id:
303+ node_id = node_id.split('@')[0]
304+
305+ return node_id + '@' + pid
306+
307+def downgrade_localfile(filename, pid):
308+ """ Every id need to be updated, i.e.:
309+ - task id
310+ - subtask id
311+ - subtask id in content
312+ """
313+ doc = openxmlfile(filename)
314+ project = doc.documentElement
315+
316+ for node in project.childNodes:
317+ node_id = node.getAttribute("id")
318+ node_id = convert_id(node_id, pid)
319+ node.setAttribute("id", node_id)
320+
321+ for sub_node in node.childNodes:
322+ if sub_node.tagName == "subtask":
323+ subtask = sub_node.firstChild.nodeValue
324+ subtask = convert_id(subtask, pid)
325+ sub_node.firstChild.nodeValue = subtask
326+
327+ elif sub_node.tagName == "content":
328+ tas = "<content>%s</content>" % sub_node.firstChild.nodeValue
329+ tas = unicode(tas).encode('UTF-8')
330+ content = xml.dom.minidom.parseString(tas)
331+
332+ for subtask_node in content.getElementsByTagName('subtask'):
333+ subtask_id = subtask_node.firstChild.nodeValue
334+ subtask_id = convert_id(subtask_id, pid)
335+ subtask_node.firstChild.nodeValue = subtask_id
336+
337+ # Convert back into string
338+ str_content = content.toxml()
339+ str_content = str_content.partition("<content>")[2]
340+ str_content = str_content.partition("</content>")[0]
341+
342+ sub_node.firstChild.nodeValue = str_content
343+
344+ # Save the work
345+ savexml(filename, doc, backup=True)
346+
347+if __name__ == "__main__":
348+ need_convert = update_projects()
349+ for filename, pid in need_convert:
350+ print "Downgrading", filename
351+ downgrade_localfile(filename, pid)

Subscribers

People subscribed via source and target branches