{% trans "Warning: This action is irreversible! All of your synced notes and notebooks will be destroyed!" %}
+
+
+
{% trans "Permanently Delete This User" %}
+
{% trans "Warning: This action is irreversible! Any data associated with your account will be destroyed!" %}
+
+
{#
{% trans "Registered Applications" %}
#}
{% 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 @@
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/accounts/views.py b/accounts/views.py
index 9a50abf..b3f5421 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -17,7 +17,7 @@
from django.utils.translation import ugettext_lazy as _
-from django.contrib.auth import get_backends
+from django.contrib.auth import get_backends, logout
from django.contrib.auth.forms import UserChangeForm, PasswordChangeForm
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, REDIRECT_FIELD_NAME
@@ -32,7 +32,8 @@
from django.conf import settings
from snowy.accounts.models import UserProfile
-from snowy.accounts.forms import InternationalizationForm, OpenIDRegistrationFormUniqueUser, EmailChangeForm, RemoveUserOpenIDForm
+from snowy.accounts.forms import InternationalizationForm, OpenIDRegistrationFormUniqueUser, EmailChangeForm, RemoveUserOpenIDForm, DeleteUserForm, ResetUserForm
+from snowy.core.utils import create_uuid
from django_openid_auth import auth
from django_openid_auth.auth import OpenIDBackend
@@ -162,11 +163,36 @@ def accounts_preferences(request, template_name='accounts/preferences.html'):
openid_form.cleaned_data['openid'].delete()
else:
openid_form = RemoveUserOpenIDForm(open_ids=open_ids)
+
+ if 'reset_form' in request.POST:
+ reset_form = ResetUserForm(request.POST)
+ if reset_form.is_valid():
+ request.user.note_set.all().delete()
+ request.user.notetag_set.all().delete()
+
+ profile = request.user.userprofile_set.all()[0]
+ profile.latest_sync_rev = -1
+ profile.current_sync_uuid = create_uuid()
+ profile.save()
+
+ messages.add_message(request, messages.SUCCESS, _("User data reset"))
+ reset_form = ResetUserForm()
+
+ if 'delete_form' in request.POST:
+ delete_form = DeleteUserForm(request.POST)
+ if delete_form.is_valid():
+ user.delete()
+ messages.add_message(request, messages.SUCCESS, _("Account deleted"))
+ logout(request)
+ return HttpResponseRedirect(reverse('snowy_index'))
+ delete_form = DeleteUserForm()
return render_to_response(template_name,
{'user': user, 'i18n_form': i18n_form,
'password_form': password_form,
'email_form' : email_form,
'open_ids' : open_ids,
- 'openid_form' : openid_form},
+ 'openid_form' : openid_form,
+ 'reset_form' : reset_form,
+ 'delete_form' : delete_form},
context_instance=RequestContext(request))
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
+#
+# 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 .
+#
+
+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 with random tags and unicode",
+ "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
+#
+# 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 .
+#
+
+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
+#
+# 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 .
+#
+
+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('%s\n\n%s' % (
+ 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 @@
'django_openid_auth',
'notes',
'mobile_notes',
+ 'export',
# System apps
'django.contrib.admin',
diff --git a/templates/base.html b/templates/base.html
index dbb19be..3d7b1ea 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -58,6 +58,12 @@
{% endblock %}
+ {% if messages %}
+ {% for message in messages %}
+