From 386fcf1d2c388f2ea551fc9fcaf8b3b8dafb95f3 Mon Sep 17 00:00:00 2001 From: Tony Young Date: Wed, 22 Dec 2010 12:32:08 +1300 Subject: [PATCH 01/12] Added additional export options (GCI task). --- accounts/templates/accounts/preferences.html | 9 ++ accounts/urls.py | 7 + export/__init__.py | 0 export/models.py | 3 + export/tests.py | 113 ++++++++++++++ export/views.py | 151 +++++++++++++++++++ 6 files changed, 283 insertions(+) create mode 100644 export/__init__.py create mode 100644 export/models.py create mode 100644 export/tests.py create mode 100644 export/views.py diff --git a/accounts/templates/accounts/preferences.html b/accounts/templates/accounts/preferences.html index 7a322a6..44298cd 100644 --- a/accounts/templates/accounts/preferences.html +++ b/accounts/templates/accounts/preferences.html @@ -94,5 +94,14 @@

{% trans "OpenID Accounts" %}

+

{% trans "Export Notes" %}

+ + + + + + +
{% trans "JSON" %}{% trans "XML" %}{% trans "Tarball" %}
+ {#

{% trans "Registered Applications" %}

#} {% endblock %} diff --git a/accounts/urls.py b/accounts/urls.py index 281b554..c3809cf 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'), @@ -42,6 +44,11 @@ name='openid-complete'), url(r'^openid/registration/$', snowy.accounts.views.openid_registration, name='openid_registration'), + + # Export URLs + url(r'^export/json', snowy.export.views.json, name='export-json'), + url(r'^export/xml', snowy.export.views.xml, name='export-xml'), + url(r'^export/tar', snowy.export.views.tar, name='export-tar'), # Registration URLs url(r'^activate/(?P\w+)/$', activate, name='registration_activate'), 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..71a8362 --- /dev/null +++ b/export/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/export/tests.py b/export/tests.py new file mode 100644 index 0000000..1c1cb85 --- /dev/null +++ b/export/tests.py @@ -0,0 +1,113 @@ +# +# 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 + +try: + import simplejson as json +except ImportError: + import json + +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": "here is a note with random tags", + "open-on-startup": True, + "last-metadata-change-date": "1970-01-01T13:00:00Z", + "title": "note 1", + "tags": [ "here", "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": "here is another note", + "open-on-startup": False, + "last-metadata-change-date": "1970-01-01T13:00:00Z", + "title": "note 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), 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_xml_export(self): + doc = parseString(snowy.export.views.xml(fake_request).content) + + for note_node in doc.childNodes[0].childNodes: + guid = note_node.getAttribute("guid") + self._assert_xml(note_node, self.grouped_data[guid]) + + def test_json_export(self): + data = json.loads(snowy.export.views.json(fake_request).content) + for guid, note in data.iteritems(): + for note_key in note: + self.assertEquals(note[note_key], self.grouped_data[guid][note_key]) + + def test_tar_export(self): + data = tarfile.TarFile(fileobj=StringIO(snowy.export.views.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/views.py b/export/views.py new file mode 100644 index 0000000..7f3c009 --- /dev/null +++ b/export/views.py @@ -0,0 +1,151 @@ +# +# 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 + +# try importing simplejson; fall back on json +try: + import simplejson as _json +except ImportError: + import json as _json + +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") + root.setAttribute("guid", note["guid"]) + + 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"], note[field])) + + 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: + elem.appendChild(doc.createTextNode(str(note[field]))) + + root.appendChild(elem) + + return root + +@login_required +def xml(request): + data = _get_data(request) + + doc = Document() + + root = doc.createElement("notes") + doc.appendChild(root) + + for note in data: + root.appendChild(_note_to_xml(doc, note)) + + response = HttpResponse(doc.toxml()) + response["Content-Type"] = "application/xml" + return response + +@login_required +def json(request): + data = _get_data(request) + + notes = {} + for note in data: + notes[note["guid"]] = dict(filter(lambda pair: pair[0] in ALLOWED_FIELDS, note.iteritems())) + notes[note["guid"]]["note-content"] = "%s\n\n%s" % (notes[note["guid"]]["title"], notes[note["guid"]]["note-content"]) + + response = HttpResponse(_json.dumps(notes, indent=4)) + response["Content-Type"] = "application/json" + return response + +@login_required +def 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() + + 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) + + 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-%d.tar" % (request.user, time.time()) + return response \ No newline at end of file From 48493a301e1e2c8e563ea87358ce22dec4f9b73a Mon Sep 17 00:00:00 2001 From: Tony Young Date: Wed, 22 Dec 2010 13:10:15 +1300 Subject: [PATCH 02/12] Made a few (mostly cosmetic) changes to exporter; fixed unit tests. --- accounts/urls.py | 5 ----- export/models.py | 3 --- export/tests.py | 13 ++++++++----- export/urls.py | 26 ++++++++++++++++++++++++++ export/views.py | 18 +++++++++--------- settings.py | 1 + urls.py | 2 ++ 7 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 export/urls.py diff --git a/accounts/urls.py b/accounts/urls.py index c3809cf..374fad5 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -44,11 +44,6 @@ name='openid-complete'), url(r'^openid/registration/$', snowy.accounts.views.openid_registration, name='openid_registration'), - - # Export URLs - url(r'^export/json', snowy.export.views.json, name='export-json'), - url(r'^export/xml', snowy.export.views.xml, name='export-xml'), - url(r'^export/tar', snowy.export.views.tar, name='export-tar'), # Registration URLs url(r'^activate/(?P\w+)/$', activate, name='registration_activate'), diff --git a/export/models.py b/export/models.py index 71a8362..e69de29 100644 --- a/export/models.py +++ b/export/models.py @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/export/tests.py b/export/tests.py index 1c1cb85..3249d9e 100644 --- a/export/tests.py +++ b/export/tests.py @@ -85,7 +85,7 @@ def _assert_xml(self, note_node, data): content = tag_wrap_expr.match(content).group(1) if tag == "text": - self.assertEquals(tag_wrap_expr.match(child_node.childNodes[0].toxml()).group(1), data["note-content"]) + 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": @@ -94,20 +94,23 @@ def _assert_xml(self, note_node, data): self.assertEquals(content, data[tag]) def test_xml_export(self): - doc = parseString(snowy.export.views.xml(fake_request).content) + doc = parseString(snowy.export.views.export_xml(fake_request).content) for note_node in doc.childNodes[0].childNodes: guid = note_node.getAttribute("guid") self._assert_xml(note_node, self.grouped_data[guid]) def test_json_export(self): - data = json.loads(snowy.export.views.json(fake_request).content) + data = json.loads(snowy.export.views.export_json(fake_request).content) for guid, note in data.iteritems(): for note_key in note: - self.assertEquals(note[note_key], self.grouped_data[guid][note_key]) + content = self.grouped_data[guid][note_key] + if note_key == "note-content": + content = "%s\n\n%s" % (self.grouped_data[guid]["title"], content) + self.assertEquals(note[note_key], content) def test_tar_export(self): - data = tarfile.TarFile(fileobj=StringIO(snowy.export.views.tar(fake_request).content), mode="r") + 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..3e898cd --- /dev/null +++ b/export/urls.py @@ -0,0 +1,26 @@ +# +# 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'^json', snowy.export.views.export_json, name='export-json'), + url(r'^xml', snowy.export.views.export_xml, name='export-xml'), + url(r'^tar', snowy.export.views.export_tar, name='export-tar'), +) diff --git a/export/views.py b/export/views.py index 7f3c009..f99e64a 100644 --- a/export/views.py +++ b/export/views.py @@ -29,9 +29,9 @@ # try importing simplejson; fall back on json try: - import simplejson as _json + import simplejson as json except ImportError: - import json as _json + import json from xml.dom.minidom import Document, parseString @@ -61,7 +61,7 @@ def _note_to_xml(doc, note): wrap_elem.setAttribute("xml:space", "preserve") # make expat parse nicely - subdoc = parseString("%s\n\n%s" % (note["title"], note[field])) + subdoc = parseString('%s\n\n%s' % (note["title"], note[field])) elem = subdoc.documentElement @@ -89,7 +89,7 @@ def _note_to_xml(doc, note): return root @login_required -def xml(request): +def export_xml(request): data = _get_data(request) doc = Document() @@ -105,7 +105,7 @@ def xml(request): return response @login_required -def json(request): +def export_json(request): data = _get_data(request) notes = {} @@ -113,12 +113,12 @@ def json(request): notes[note["guid"]] = dict(filter(lambda pair: pair[0] in ALLOWED_FIELDS, note.iteritems())) notes[note["guid"]]["note-content"] = "%s\n\n%s" % (notes[note["guid"]]["title"], notes[note["guid"]]["note-content"]) - response = HttpResponse(_json.dumps(notes, indent=4)) + response = HttpResponse(json.dumps(notes, indent=4)) response["Content-Type"] = "application/json" return response @login_required -def tar(request): +def export_tar(request): data = _get_data(request) arch_file = StringIO() @@ -147,5 +147,5 @@ def tar(request): response = HttpResponse(arch_file.getvalue()) response["Content-Type"] = "application/x-tar" - response["Content-Disposition"] = "attachment; filename=snowy-%s-%d.tar" % (request.user, time.time()) - return response \ No newline at end of file + 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/urls.py b/urls.py index 923cfb7..79544d1 100644 --- a/urls.py +++ b/urls.py @@ -33,6 +33,8 @@ (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\w+)/$', 'snowy.views.user_index', name="user_index"), From 42858184c37bc648503a8eff4ccaccacff11b450 Mon Sep 17 00:00:00 2001 From: Tony Young Date: Wed, 22 Dec 2010 19:46:47 +1300 Subject: [PATCH 03/12] Fixed unicode support when exporting to XML/tarball. --- export/tests.py | 44 ++++++++++++++++++++++---------------------- export/views.py | 12 +++++++++--- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/export/tests.py b/export/tests.py index 3249d9e..e123d7d 100644 --- a/export/tests.py +++ b/export/tests.py @@ -1,4 +1,4 @@ -# +# # Copyright (c) 2010 Tony Young # # This program is free software: you can redistribute it and/or modify it under @@ -44,27 +44,27 @@ 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": "here is a note with random tags", - "open-on-startup": True, - "last-metadata-change-date": "1970-01-01T13:00:00Z", - "title": "note 1", - "tags": [ "here", "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": "here is another note", - "open-on-startup": False, - "last-metadata-change-date": "1970-01-01T13:00:00Z", - "title": "note 2", - "tags": [ "here", "are", "some", "tags", "too" ], - "create-date": "1970-01-01T13:00:00Z", - "last-change-date": "1970-01-01T13:00:00Z" - } - ] + { + "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 diff --git a/export/views.py b/export/views.py index f99e64a..511408b 100644 --- a/export/views.py +++ b/export/views.py @@ -61,7 +61,10 @@ def _note_to_xml(doc, note): wrap_elem.setAttribute("xml:space", "preserve") # make expat parse nicely - subdoc = parseString('%s\n\n%s' % (note["title"], note[field])) + subdoc = parseString('%s\n\n%s' % ( + note["title"].encode("utf-8"), + note[field].encode("utf-8") + )) elem = subdoc.documentElement @@ -82,7 +85,10 @@ def _note_to_xml(doc, note): tag_elem.appendChild(doc.createTextNode(tag)) elem.appendChild(tag_elem) else: - elem.appendChild(doc.createTextNode(str(note[field]))) + content = note[field] + if not isinstance(content, unicode): + content = str(content) + elem.appendChild(doc.createTextNode(content)) root.appendChild(elem) @@ -129,7 +135,7 @@ def export_tar(request): root = _note_to_xml(doc, note) doc.appendChild(root) - note_data = doc.toxml() + note_data = doc.toxml().encode("utf-8") note_file = StringIO() note_file.write(note_data) From cf2393fdc29c59be64847b1b321bde7aec1538b1 Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 08:17:01 +1300 Subject: [PATCH 04/12] Removed XML/JSON exporter from export application. --- accounts/templates/accounts/preferences.html | 4 +-- export/tests.py | 21 ------------ export/urls.py | 2 -- export/views.py | 35 -------------------- 4 files changed, 2 insertions(+), 60 deletions(-) diff --git a/accounts/templates/accounts/preferences.html b/accounts/templates/accounts/preferences.html index 44298cd..bb4a39e 100644 --- a/accounts/templates/accounts/preferences.html +++ b/accounts/templates/accounts/preferences.html @@ -97,8 +97,8 @@

{% trans "OpenID Accounts" %}

{% trans "Export Notes" %}

- - + +
{% trans "JSON" %}{% trans "XML" %}{% trans "JSON" %}{% trans "XML" %} {% trans "Tarball" %}
diff --git a/export/tests.py b/export/tests.py index e123d7d..1ba1e31 100644 --- a/export/tests.py +++ b/export/tests.py @@ -19,11 +19,6 @@ import snowy.export.views -try: - import simplejson as json -except ImportError: - import json - import datetime import re import tarfile @@ -93,22 +88,6 @@ def _assert_xml(self, note_node, data): else: self.assertEquals(content, data[tag]) - def test_xml_export(self): - doc = parseString(snowy.export.views.export_xml(fake_request).content) - - for note_node in doc.childNodes[0].childNodes: - guid = note_node.getAttribute("guid") - self._assert_xml(note_node, self.grouped_data[guid]) - - def test_json_export(self): - data = json.loads(snowy.export.views.export_json(fake_request).content) - for guid, note in data.iteritems(): - for note_key in note: - content = self.grouped_data[guid][note_key] - if note_key == "note-content": - content = "%s\n\n%s" % (self.grouped_data[guid]["title"], content) - self.assertEquals(note[note_key], content) - def test_tar_export(self): data = tarfile.TarFile(fileobj=StringIO(snowy.export.views.export_tar(fake_request).content), mode="r") for info in data: diff --git a/export/urls.py b/export/urls.py index 3e898cd..0d756da 100644 --- a/export/urls.py +++ b/export/urls.py @@ -20,7 +20,5 @@ import snowy.export.views urlpatterns = patterns('', - url(r'^json', snowy.export.views.export_json, name='export-json'), - url(r'^xml', snowy.export.views.export_xml, name='export-xml'), url(r'^tar', snowy.export.views.export_tar, name='export-tar'), ) diff --git a/export/views.py b/export/views.py index 511408b..9d60731 100644 --- a/export/views.py +++ b/export/views.py @@ -27,12 +27,6 @@ import time -# try importing simplejson; fall back on json -try: - import simplejson as json -except ImportError: - import json - from xml.dom.minidom import Document, parseString try: @@ -94,35 +88,6 @@ def _note_to_xml(doc, note): return root -@login_required -def export_xml(request): - data = _get_data(request) - - doc = Document() - - root = doc.createElement("notes") - doc.appendChild(root) - - for note in data: - root.appendChild(_note_to_xml(doc, note)) - - response = HttpResponse(doc.toxml()) - response["Content-Type"] = "application/xml" - return response - -@login_required -def export_json(request): - data = _get_data(request) - - notes = {} - for note in data: - notes[note["guid"]] = dict(filter(lambda pair: pair[0] in ALLOWED_FIELDS, note.iteritems())) - notes[note["guid"]]["note-content"] = "%s\n\n%s" % (notes[note["guid"]]["title"], notes[note["guid"]]["note-content"]) - - response = HttpResponse(json.dumps(notes, indent=4)) - response["Content-Type"] = "application/json" - return response - @login_required def export_tar(request): data = _get_data(request) From 2ce3a32985481d2cafb7776fef7c1d13fbdc3942 Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 10:05:22 +1300 Subject: [PATCH 05/12] Added user deletion support. --- accounts/forms.py | 7 +++++- accounts/templates/accounts/preferences.html | 24 ++++++++++++++------ accounts/views.py | 19 +++++++++++++--- templates/base.html | 6 +++++ 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/accounts/forms.py b/accounts/forms.py index 79f86a3..9d4d5f2 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -16,7 +16,7 @@ # from django.contrib.auth.models import User -from django.forms import ModelChoiceField +from django.forms import ModelChoiceField, BooleanField from registration.forms import RegistrationFormUniqueEmail from django.utils.translation import ugettext_lazy as _ from recaptcha_django import ReCaptchaField @@ -109,3 +109,8 @@ def __init__(self, *args, **kwargs): super(RemoveUserOpenIDForm, self).__init__(*args, **kwargs) self.fields['openid'] = UserOpenIDChoiceField(open_ids, required=True, label=_('Delete OpenID account')) + +class DeleteUserForm(forms.Form): + def __init__(self, *args, **kwargs): + super(DeleteUserForm, self).__init__(*args, **kwargs) + self.fields['confirm'] = BooleanField(required=True, label=_('Confirm account deletion')) diff --git a/accounts/templates/accounts/preferences.html b/accounts/templates/accounts/preferences.html index bb4a39e..0fb4c49 100644 --- a/accounts/templates/accounts/preferences.html +++ b/accounts/templates/accounts/preferences.html @@ -14,13 +14,6 @@ {% block content %} -{% if messages %} -{% for message in messages %} -{{ message }} -
-{% endfor %} -{% endif %} -

{% trans "Preferences" %}

{% if user.has_usable_password %}

{% trans "Change your password" %}

@@ -103,5 +96,22 @@

{% trans "Export Notes" %}

+

{% trans "Delete User" %}

+
{% trans "Warning: This action is irreversible! Any data associated with your account will be destroyed!" %}
+
+ + {{ delete_form.as_table }} + + + + + + +
+ +
+ +
+ {#

{% trans "Registered Applications" %}

#} {% endblock %} diff --git a/accounts/views.py b/accounts/views.py index 9a50abf..114e5b6 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -15,9 +15,11 @@ # along with this program. If not, see . # +from itertools import chain + 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 +34,7 @@ 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 from django_openid_auth import auth from django_openid_auth.auth import OpenIDBackend @@ -162,11 +164,22 @@ def accounts_preferences(request, template_name='accounts/preferences.html'): openid_form.cleaned_data['openid'].delete() else: openid_form = RemoveUserOpenIDForm(open_ids=open_ids) + + 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')) + else: + 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, + 'delete_form' : delete_form}, context_instance=RequestContext(request)) 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 %} + {{ message }} +
+ {% endfor %} + {% endif %} {% block content %} {% endblock %} From 40520f1c4553b1e3fbfa9d22753a6a95fe63653c Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 14:39:19 +1300 Subject: [PATCH 06/12] Fixed mtime on files in tar export. --- export/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/export/views.py b/export/views.py index 9d60731..cbaaab1 100644 --- a/export/views.py +++ b/export/views.py @@ -26,6 +26,7 @@ import tarfile import time +from dateutil.parser import parse as parse_iso_time from xml.dom.minidom import Document, parseString @@ -108,6 +109,7 @@ def export_tar(request): 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, From 31deaf849bb3ca35aad35917b4f7e19ddfdabe26 Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 14:59:37 +1300 Subject: [PATCH 07/12] Made XML note file generation closer to specification. --- export/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/export/views.py b/export/views.py index cbaaab1..a022b1b 100644 --- a/export/views.py +++ b/export/views.py @@ -48,7 +48,6 @@ def _note_to_xml(doc, note): root.setAttribute("xmlns:link", "http://beatniksoftware.com/tomboy/link") root.setAttribute("xmlns:size", "http://beatniksoftware.com/tomboy/size") root.setAttribute("version", "0.3") - root.setAttribute("guid", note["guid"]) for field in ALLOWED_FIELDS: if field == "note-content": @@ -101,7 +100,7 @@ def export_tar(request): root = _note_to_xml(doc, note) doc.appendChild(root) - note_data = doc.toxml().encode("utf-8") + note_data = doc.toxml(encoding='utf-8') note_file = StringIO() note_file.write(note_data) From 8d180fb5a88d9e4640031e649267903452958e14 Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 15:28:51 +1300 Subject: [PATCH 08/12] Added account reset functionality to Snowy. --- accounts/forms.py | 5 +++++ accounts/templates/accounts/preferences.html | 18 +++++++++++++++++ accounts/views.py | 21 +++++++++++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/accounts/forms.py b/accounts/forms.py index 9d4d5f2..f7dab17 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -110,6 +110,11 @@ def __init__(self, *args, **kwargs): self.fields['openid'] = UserOpenIDChoiceField(open_ids, required=True, label=_('Delete OpenID account')) +class ResetUserForm(forms.Form): + def __init__(self, *args, **kwargs): + super(ResetUserForm, self).__init__(*args, **kwargs) + self.fields['confirm'] = BooleanField(required=True, label=_('Confirm account reset')) + class DeleteUserForm(forms.Form): def __init__(self, *args, **kwargs): super(DeleteUserForm, self).__init__(*args, **kwargs) diff --git a/accounts/templates/accounts/preferences.html b/accounts/templates/accounts/preferences.html index 0fb4c49..77577ae 100644 --- a/accounts/templates/accounts/preferences.html +++ b/accounts/templates/accounts/preferences.html @@ -96,6 +96,24 @@

{% trans "Export Notes" %}

+ +

{% trans "Reset User" %}

+
{% trans "Warning: This action is irreversible! Any data associated with your account will be destroyed!" %}
+
+ + {{ reset_form.as_table }} + + + + + + +
+ +
+ +
+

{% trans "Delete User" %}

{% trans "Warning: This action is irreversible! Any data associated with your account will be destroyed!" %}
diff --git a/accounts/views.py b/accounts/views.py index 114e5b6..d185077 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -34,7 +34,8 @@ from django.conf import settings from snowy.accounts.models import UserProfile -from snowy.accounts.forms import InternationalizationForm, OpenIDRegistrationFormUniqueUser, EmailChangeForm, RemoveUserOpenIDForm, DeleteUserForm +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 @@ -165,6 +166,20 @@ def accounts_preferences(request, template_name='accounts/preferences.html'): else: openid_form = RemoveUserOpenIDForm(open_ids=open_ids) + if 'reset_form' in request.POST: + reset_form = ResetUserForm(request.POST) + if reset_form.is_valid(): + for data in chain(request.user.note_set.all(), request.user.notetag_set.all()): + data.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(): @@ -172,8 +187,7 @@ def accounts_preferences(request, template_name='accounts/preferences.html'): messages.add_message(request, messages.SUCCESS, _("Account deleted")) logout(request) return HttpResponseRedirect(reverse('snowy_index')) - else: - delete_form = DeleteUserForm() + delete_form = DeleteUserForm() return render_to_response(template_name, {'user': user, 'i18n_form': i18n_form, @@ -181,5 +195,6 @@ def accounts_preferences(request, template_name='accounts/preferences.html'): 'email_form' : email_form, 'open_ids' : open_ids, 'openid_form' : openid_form, + 'reset_form' : reset_form, 'delete_form' : delete_form}, context_instance=RequestContext(request)) From 938a9b2fe2374440ff60ec0cec2b01f1dfb21df0 Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 15:43:35 +1300 Subject: [PATCH 09/12] Added cosmetic changes, as well as all().delete() fix Sandy recommended. --- accounts/templates/accounts/preferences.html | 37 ++++++++++++++++---- accounts/views.py | 4 +-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/accounts/templates/accounts/preferences.html b/accounts/templates/accounts/preferences.html index 77577ae..69e7ead 100644 --- a/accounts/templates/accounts/preferences.html +++ b/accounts/templates/accounts/preferences.html @@ -98,15 +98,17 @@

{% trans "Export Notes" %}

{% trans "Reset User" %}

-
{% trans "Warning: This action is irreversible! Any data associated with your account will be destroyed!" %}
+
{% trans "Warning: This action is irreversible! All of your synced notes and notebooks will be destroyed!" %}
- {{ reset_form.as_table }} + + {{ reset_form.as_table }} + @@ -115,15 +117,17 @@

{% trans "Reset User" %}

{% trans "Delete User" %}

-
{% trans "Warning: This action is irreversible! Any data associated with your account will be destroyed!" %}
+
{% trans "Warning: This action is irreversible! Any data associated with your account will be destroyed!" %}
- +
- {{ delete_form.as_table }} + + {{ delete_form.as_table }} + @@ -131,5 +135,24 @@

{% trans "Delete User" %}

+ + {#

{% trans "Registered Applications" %}

#} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py index d185077..a0adff9 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -169,8 +169,8 @@ def accounts_preferences(request, template_name='accounts/preferences.html'): if 'reset_form' in request.POST: reset_form = ResetUserForm(request.POST) if reset_form.is_valid(): - for data in chain(request.user.note_set.all(), request.user.notetag_set.all()): - data.delete() + request.user.note_set.all().delete() + request.user.notetag_set.all().delete() profile = request.user.userprofile_set.all()[0] profile.latest_sync_rev = -1 From 96a68b12dfbddab94c6ddae8dc56e504d898febe Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 15:45:45 +1300 Subject: [PATCH 10/12] Whoops, don't need that itertools import any more. --- accounts/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index a0adff9..b3f5421 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -15,8 +15,6 @@ # along with this program. If not, see . # -from itertools import chain - from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import get_backends, logout From 89dc0565476fbd780b9bf02eb18e28b96b717e45 Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 15:58:48 +1300 Subject: [PATCH 11/12] More descriptive text in preferences for delete/reset. --- accounts/forms.py | 4 ++-- accounts/templates/accounts/preferences.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/accounts/forms.py b/accounts/forms.py index f7dab17..de72ab9 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -113,9 +113,9 @@ def __init__(self, *args, **kwargs): class ResetUserForm(forms.Form): def __init__(self, *args, **kwargs): super(ResetUserForm, self).__init__(*args, **kwargs) - self.fields['confirm'] = BooleanField(required=True, label=_('Confirm account reset')) + self.fields['confirm_reset'] = BooleanField(required=True, label=_('Confirm account reset')) class DeleteUserForm(forms.Form): def __init__(self, *args, **kwargs): super(DeleteUserForm, self).__init__(*args, **kwargs) - self.fields['confirm'] = BooleanField(required=True, label=_('Confirm account deletion')) + self.fields['confirm_delete'] = BooleanField(required=True, label=_('Confirm account deletion')) diff --git a/accounts/templates/accounts/preferences.html b/accounts/templates/accounts/preferences.html index 69e7ead..5e69223 100644 --- a/accounts/templates/accounts/preferences.html +++ b/accounts/templates/accounts/preferences.html @@ -97,7 +97,7 @@

{% trans "Export Notes" %}

- +
-

{% trans "Reset User" %}

+

{% trans "Reset All Data" %}

{% trans "Warning: This action is irreversible! All of your synced notes and notebooks will be destroyed!" %}
@@ -116,7 +116,7 @@

{% trans "Reset User" %}

-

{% trans "Delete User" %}

+

{% trans "Permanently Delete This User" %}

{% trans "Warning: This action is irreversible! Any data associated with your account will be destroyed!" %}
From 46d56641c5e8e9486c255748700337eeb3c2bd9b Mon Sep 17 00:00:00 2001 From: Tony Young Date: Thu, 23 Dec 2010 16:12:45 +1300 Subject: [PATCH 12/12] Moved JavaScript from into . --- accounts/templates/accounts/preferences.html | 36 +++++++++----------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/accounts/templates/accounts/preferences.html b/accounts/templates/accounts/preferences.html index 5e69223..6056213 100644 --- a/accounts/templates/accounts/preferences.html +++ b/accounts/templates/accounts/preferences.html @@ -8,6 +8,21 @@ // clear the password form $(document).ready(function() { $('.input-form input[type="password"]').val("") + + $(".reset-confirm").hide(); + $(".delete-confirm").hide(); + + $("#reset-btn").bind("click", function(evt) { + $(".reset-confirm").fadeIn(); + $(this).unbind("click"); + evt.preventDefault(); + }); + + $("#delete-btn").bind("click", function(evt) { + $(".delete-confirm").fadeIn(); + $(this).unbind("click"); + evt.preventDefault(); + }); }); {% endblock %} @@ -135,24 +150,5 @@

{% trans "Permanently Delete This User" %}

- - {#

{% trans "Registered Applications" %}

#} -{% endblock %} \ No newline at end of file +{% endblock %}