[snowy] Let user export data to JSON or tar of .note files



commit fafd5d14824f676130c62f596a9f9601bf59b2e8
Author: Tony Young <rofflwaffls gmail com>
Date:   Wed Dec 22 12:32:08 2010 +1300

    Let user export data to JSON or tar of .note files
    
    https://bugzilla.gnome.org/show_bug.cgi?id=629590

 accounts/templates/accounts/preferences.html |   12 +++
 accounts/urls.py                             |    2 +
 export/tests.py                              |   95 ++++++++++++++++++++
 export/urls.py                               |   24 +++++
 export/views.py                              |  123 ++++++++++++++++++++++++++
 settings.py                                  |    1 +
 urls.py                                      |    2 +
 7 files changed, 259 insertions(+), 0 deletions(-)
---
diff --git a/accounts/templates/accounts/preferences.html b/accounts/templates/accounts/preferences.html
index 7a322a6..b82fe58 100644
--- a/accounts/templates/accounts/preferences.html
+++ b/accounts/templates/accounts/preferences.html
@@ -94,5 +94,17 @@
     <input type="hidden" name="openid_form" value="1" />
 </form>
 
+<h3>{% trans "Export Notes" %}</h3>
+<table class="input-form">
+    <tr>
+        <th><a href="{% url note_api_index username=request.user %}?include_notes=true">{% trans "JSON (see API docs)" %}</a></th>
+        {% comment %}
+        {# Providing API output in XML format may just be confusing #}
+        <th><a href="{% url note_api_index username=request.user %}?include_notes=true&format=xml">{% trans "XML" %}</a></th>
+        {% endcomment %}
+        <th><a href="{% url export-tar %}">{% trans "Tarball of .note files (EXPERIMENTAL!)" %}</a></th>
+    </tr>
+</table>
+
 {#<h3>{% trans "Registered Applications" %}</h3>#}
 {% endblock %}
diff --git a/accounts/urls.py b/accounts/urls.py
index 281b554..374fad5 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -27,6 +27,8 @@ from registration.views import register
 import snowy.accounts.views
 import django_openid_auth.views
 
+import snowy.export.views
+
 urlpatterns = patterns('',
     url(r'^preferences/$', 'snowy.accounts.views.accounts_preferences',
         name='preferences'),
diff --git a/export/__init__.py b/export/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/export/models.py b/export/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/export/tests.py b/export/tests.py
new file mode 100644
index 0000000..1ba1e31
--- /dev/null
+++ b/export/tests.py
@@ -0,0 +1,95 @@
+#
+# Copyright (c) 2010 Tony Young <rofflwaffls gmail com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero 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 Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from django.test import TestCase
+
+import snowy.export.views
+
+import datetime
+import re
+import tarfile
+from xml.dom.minidom import parseString, parse
+
+try:
+    from cStringIO import StringIO
+except:
+    from StringIO import StringIO
+
+class fake_request(object):
+    class user:
+        @staticmethod
+        def is_authenticated():
+            return True
+
+class ExportTest(TestCase):
+    def setUp(self):
+        # monkey patch snowy.export._get_data to return what we want
+        self.data = [
+            {
+                "guid" : "00000000-0000-0000-0000-000000000000",
+                "note-content": u"here is a note <bold>with random tags</bold>ã??ï½?ï½?ï½?ã??ï½?ï½?ï½?ï½?ï½?ï½?ï½?",
+                "open-on-startup": True,
+                "last-metadata-change-date": "1970-01-01T13:00:00Z",
+                "title": u"note 壱",
+                "tags": [ u"herë", "are", "some", "tags" ],
+                "create-date": "1970-01-01T13:00:00Z",
+                "last-change-date": "1970-01-01T13:00:00Z"
+            },
+            {
+                "guid" : "00000000-0000-0000-0000-000000000001",
+                "note-content": u"here is another note with äçcèñts",
+                "open-on-startup": False,
+                "last-metadata-change-date": "1970-01-01T13:00:00Z",
+                "title": u"ã??ã?¼ã?? 2",
+                "tags": [ "here", "are", "some", "tags", "too" ],
+                "create-date": "1970-01-01T13:00:00Z",
+                "last-change-date": "1970-01-01T13:00:00Z"
+            }
+        ]
+
+        self.grouped_data = dict([
+            (note["guid"], note) for note in self.data
+        ])
+
+        def _get_data(request):
+            return self.data
+        snowy.export.views._get_data = _get_data
+
+    def _assert_xml(self, note_node, data):
+        tag_wrap_expr = re.compile(r"\<.+?\>(.*)\</.+?\>", re.MULTILINE | re.DOTALL)
+
+        guid = note_node.getAttribute("guid")
+        for child_node in note_node.childNodes:
+            tag = child_node.tagName
+
+            content = child_node.toxml()
+            content = tag_wrap_expr.match(content).group(1)
+
+            if tag == "text":
+                self.assertEquals(tag_wrap_expr.match(child_node.childNodes[0].toxml()).group(1), "%s\n\n%s" % (data["title"], data["note-content"]))
+            elif tag == "tags":
+                self.assertEquals([ tag.childNodes[0].toxml() for tag in child_node.childNodes ], data["tags"])
+            elif tag == "open-on-startup":
+                self.assertEquals(content == "True" and True or False, data[tag])
+            else:
+                self.assertEquals(content, data[tag])
+
+    def test_tar_export(self):
+        data = tarfile.TarFile(fileobj=StringIO(snowy.export.views.export_tar(fake_request).content), mode="r")
+        for info in data:
+            doc = parse(data.extractfile(info.name))
+            self._assert_xml(doc.childNodes[0], self.grouped_data[info.name.split(".")[0]])
diff --git a/export/urls.py b/export/urls.py
new file mode 100644
index 0000000..0d756da
--- /dev/null
+++ b/export/urls.py
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2010 Tony Young <rofflwaffls gmail com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero 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 Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from django.conf.urls.defaults import *
+
+import snowy.export.views
+
+urlpatterns = patterns('',
+    url(r'^tar', snowy.export.views.export_tar, name='export-tar'),
+)
diff --git a/export/views.py b/export/views.py
new file mode 100644
index 0000000..a022b1b
--- /dev/null
+++ b/export/views.py
@@ -0,0 +1,123 @@
+#
+# Copyright (c) 2010 Tony Young <rofflwaffls gmail com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero 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 Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from django.http import HttpResponse
+
+from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required
+
+from snowy.api.handlers import describe_note
+from snowy.notes.models import Note
+
+import tarfile
+
+import time
+from dateutil.parser import parse as parse_iso_time
+
+from xml.dom.minidom import Document, parseString
+
+try:
+    from cStringIO import StringIO
+except:
+    from StringIO import StringIO
+
+# list of allowed fields
+ALLOWED_FIELDS = [ "title", "note-content", "last-change-date", "last-metadata-change-date", "create-date", "tags", "open-on-startup" ]
+
+def _get_data(request):
+    notes = Note.objects.user_viewable(request.user, User.objects.get(username=request.user))
+    return [describe_note(n) for n in notes]
+
+def _note_to_xml(doc, note):
+    root = doc.createElement("note")
+    root.setAttribute("xmlns", "http://beatniksoftware.com/tomboy";)
+    root.setAttribute("xmlns:link", "http://beatniksoftware.com/tomboy/link";)
+    root.setAttribute("xmlns:size", "http://beatniksoftware.com/tomboy/size";)
+    root.setAttribute("version", "0.3")
+
+    for field in ALLOWED_FIELDS:
+        if field == "note-content":
+            wrap_elem = doc.createElement("text")
+            wrap_elem.setAttribute("xml:space", "preserve")
+
+            # make expat parse nicely
+            subdoc = parseString('<note-content xmlns:link="urn:tempuri" xmlns:size="urn:tempuri">%s\n\n%s</note-content>' % (
+                note["title"].encode("utf-8"),
+                note[field].encode("utf-8")
+            ))
+
+            elem = subdoc.documentElement
+
+            # quietly get rid of temporary namespaces
+            elem.removeAttribute("xmlns:link")
+            elem.removeAttribute("xmlns:size")
+            
+            elem.setAttribute("version", "0.1")
+
+            wrap_elem.appendChild(elem)
+            elem = wrap_elem
+        else:
+            elem = doc.createElement(field)
+
+            if field == "tags":
+                for tag in note[field]:
+                    tag_elem = doc.createElement("tag")
+                    tag_elem.appendChild(doc.createTextNode(tag))
+                    elem.appendChild(tag_elem)
+            else:
+                content = note[field]
+                if not isinstance(content, unicode):
+                    content = str(content)
+                elem.appendChild(doc.createTextNode(content))
+
+        root.appendChild(elem)
+
+    return root
+
+ login_required
+def export_tar(request):
+    data = _get_data(request)
+
+    arch_file = StringIO()
+    arch = tarfile.TarFile(fileobj=arch_file, mode="w")
+
+    for note in data:
+        doc = Document()
+        root = _note_to_xml(doc, note)
+        doc.appendChild(root)
+
+        note_data = doc.toxml(encoding='utf-8')
+
+        note_file = StringIO()
+        note_file.write(note_data)
+        note_file.seek(0)
+
+        note_info = tarfile.TarInfo("%s.note" % note["guid"])
+        note_info.size = len(note_data)
+        note_info.mtime = time.mktime(parse_iso_time(note["last-change-date"]).timetuple())
+
+        arch.addfile(
+            tarinfo=note_info,
+            fileobj=note_file
+        )
+
+    arch.close()
+
+    response = HttpResponse(arch_file.getvalue())
+    response["Content-Type"] = "application/x-tar"
+    response["Content-Disposition"] = "attachment; filename=snowy-%s-%s.tar" % (request.user, time.strftime("%Y-%m-%d"))
+    return response
diff --git a/settings.py b/settings.py
index e21dc06..6c5ccc0 100644
--- a/settings.py
+++ b/settings.py
@@ -127,6 +127,7 @@ INSTALLED_APPS = [
     'django_openid_auth',
     'notes',
     'mobile_notes',
+    'export',
 
     # System apps
     'django.contrib.admin',
diff --git a/urls.py b/urls.py
index 923cfb7..79544d1 100644
--- a/urls.py
+++ b/urls.py
@@ -33,6 +33,8 @@ urlpatterns = patterns('',
     (r'^admin/doc/', include('django.contrib.admindocs.urls')),
     (r'^admin/', include(admin.site.urls)),
 
+    (r'^export/', include('snowy.export.urls')),
+
     (r'^mobile/', include('snowy.mobile_notes.urls')),
 
     url(r'^(?P<username>\w+)/$', 'snowy.views.user_index', name="user_index"),



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]