diff options
-rw-r--r-- | mediagoblin/db/models.py | 73 | ||||
-rw-r--r-- | mediagoblin/edit/routing.py | 2 | ||||
-rw-r--r-- | mediagoblin/edit/views.py | 31 | ||||
-rw-r--r-- | mediagoblin/static/css/base.css | 6 | ||||
-rw-r--r-- | mediagoblin/templates/mediagoblin/edit/delete_account.html | 43 | ||||
-rw-r--r-- | mediagoblin/templates/mediagoblin/edit/edit_account.html | 4 | ||||
-rw-r--r-- | mediagoblin/tests/test_edit.py | 18 | ||||
-rw-r--r-- | mediagoblin/user_pages/views.py | 17 |
8 files changed, 173 insertions, 21 deletions
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index ea915ae5..782bf869 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -18,7 +18,7 @@ TODO: indexes on foreignkeys, where useful. """ - +import logging import datetime import sys @@ -34,6 +34,7 @@ from sqlalchemy.util import memoized_property from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded from mediagoblin.db.base import Base, DictReadAttrProxy, Session from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin +from mediagoblin.tools.files import delete_media_files # It's actually kind of annoying how sqlalchemy-migrate does this, if # I understand it right, but whatever. Anyway, don't remove this :P @@ -42,6 +43,8 @@ from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, # this import-based meddling... from migrate import changeset +_log = logging.getLogger(__name__) + class User(Base, UserMixin): """ @@ -78,6 +81,27 @@ class User(Base, UserMixin): 'admin' if self.is_admin else 'user', self.username) + def delete(self, **kwargs): + """Deletes a User and all related entries/comments/files/...""" + # Delete this user's Collections and all contained CollectionItems + for collection in self.collections: + collection.delete(commit=False) + + media_entries = MediaEntry.query.filter(MediaEntry.uploader == self.id) + for media in media_entries: + # TODO: Make sure that "MediaEntry.delete()" also deletes + # all related files/Comments + media.delete(del_orphan_tags=False, commit=False) + + # Delete now unused tags + # TODO: import here due to cyclic imports!!! This cries for refactoring + from mediagoblin.db.util import clean_orphan_tags + clean_orphan_tags(commit=False) + + # Delete user, pass through commit=False/True in kwargs + super(User, self).delete(**kwargs) + _log.info('Deleted user "{0}" account'.format(self.username)) + class MediaEntry(Base, MediaEntryMixin): """ @@ -122,7 +146,6 @@ class MediaEntry(Base, MediaEntryMixin): ) attachment_files_helper = relationship("MediaAttachmentFile", - cascade="all, delete-orphan", order_by="MediaAttachmentFile.created" ) attachment_files = association_proxy("attachment_files_helper", "dict_view", @@ -131,7 +154,7 @@ class MediaEntry(Base, MediaEntryMixin): ) tags_helper = relationship("MediaTag", - cascade="all, delete-orphan" + cascade="all, delete-orphan" # should be automatically deleted ) tags = association_proxy("tags_helper", "dict_view", creator=lambda v: MediaTag(name=v["name"], slug=v["slug"]) @@ -216,6 +239,37 @@ class MediaEntry(Base, MediaEntryMixin): id=self.id, title=safe_title) + def delete(self, del_orphan_tags=True, **kwargs): + """Delete MediaEntry and all related files/attachments/comments + + This will *not* automatically delete unused collections, which + can remain empty... + + :param del_orphan_tags: True/false if we delete unused Tags too + :param commit: True/False if this should end the db transaction""" + # User's CollectionItems are automatically deleted via "cascade". + # Delete all the associated comments + for comment in self.get_comments(): + comment.delete(commit=False) + + # Delete all related files/attachments + try: + delete_media_files(self) + except OSError, error: + # Returns list of files we failed to delete + _log.error('No such files from the user "{1}" to delete: ' + '{0}'.format(str(error), self.get_uploader)) + _log.info('Deleted Media entry id "{0}"'.format(self.id)) + # Related MediaTag's are automatically cleaned, but we might + # want to clean out unused Tag's too. + if del_orphan_tags: + # TODO: Import here due to cyclic imports!!! + # This cries for refactoring + from mediagoblin.db.util import clean_orphan_tags + clean_orphan_tags(commit=False) + # pass through commit=False/True in kwargs + super(MediaEntry, self).delete(**kwargs) + class FileKeynames(Base): """ @@ -344,6 +398,10 @@ class MediaComment(Base, MediaCommentMixin): class Collection(Base, CollectionMixin): + """An 'album' or 'set' of media by a user. + + On deletion, contained CollectionItems get automatically reaped via + SQL cascade""" __tablename__ = "core__collections" id = Column(Integer, primary_key=True) @@ -353,11 +411,13 @@ class Collection(Base, CollectionMixin): index=True) description = Column(UnicodeText) creator = Column(Integer, ForeignKey(User.id), nullable=False) + # TODO: No of items in Collection. Badly named, can we migrate to num_items? items = Column(Integer, default=0) - get_creator = relationship(User) + get_creator = relationship(User, backref="collections") def get_collection_items(self, ascending=False): + #TODO, is this still needed with self.collection_items being available? order_col = CollectionItem.position if not ascending: order_col = desc(order_col) @@ -375,7 +435,10 @@ class CollectionItem(Base, CollectionItemMixin): note = Column(UnicodeText, nullable=True) added = Column(DateTime, nullable=False, default=datetime.datetime.now) position = Column(Integer) - in_collection = relationship("Collection") + in_collection = relationship("Collection", + backref=backref( + "collection_items", + cascade="all, delete-orphan")) get_media_entry = relationship(MediaEntry) diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py index d382e549..035a766f 100644 --- a/mediagoblin/edit/routing.py +++ b/mediagoblin/edit/routing.py @@ -22,3 +22,5 @@ add_route('mediagoblin.edit.legacy_edit_profile', '/edit/profile/', 'mediagoblin.edit.views:legacy_edit_profile') add_route('mediagoblin.edit.account', '/edit/account/', 'mediagoblin.edit.views:edit_account') +add_route('mediagoblin.edit.delete_account', '/edit/account/delete/', + 'mediagoblin.edit.views:delete_account') diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 9b7cab46..c656c63f 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -267,6 +267,37 @@ def edit_account(request): @require_active_login +def delete_account(request): + """Delete a user completely""" + user = request.user + if request.method == 'POST': + if request.form.get(u'confirmed'): + # Form submitted and confirmed. Actually delete the user account + # Log out user and delete cookies etc. + # TODO: Should we be using MG.auth.views.py:logout for this? + request.session.delete() + + # Delete user account and all related media files etc.... + request.user.delete() + + # We should send a message that the user has been deleted + # successfully. But we just deleted the session, so we + # can't... + return redirect(request, 'index') + + else: # Did not check the confirmation box... + messages.add_message( + request, messages.WARNING, + _('You need to confirm the deletion of your account.')) + + # No POST submission or not confirmed, just show page + return render_to_response( + request, + 'mediagoblin/edit/delete_account.html', + {'user': user}) + + +@require_active_login @user_may_alter_collection @get_user_collection def edit_collection(request, collection): diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css index 2085cfc9..81a90615 100644 --- a/mediagoblin/static/css/base.css +++ b/mediagoblin/static/css/base.css @@ -331,6 +331,12 @@ textarea#description, textarea#bio { height: 100px; } +.delete { + margin-top: 36px; + display: block; + text-align: center; +} + /* comments */ .comment_wrapper { diff --git a/mediagoblin/templates/mediagoblin/edit/delete_account.html b/mediagoblin/templates/mediagoblin/edit/delete_account.html new file mode 100644 index 00000000..6d56d77c --- /dev/null +++ b/mediagoblin/templates/mediagoblin/edit/delete_account.html @@ -0,0 +1,43 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# 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/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_content %} + + <form action="{{ request.urlgen('mediagoblin.edit.delete_account') }}" + method="POST" enctype="multipart/form-data"> + <div class="form_box"> + <h1>Really delete user '{{ user.username }}' and all related media/comments? + </h1> + <p class="delete_checkbox_box"> + <input type="checkbox" name="confirmed"/> + <label for="confirmed">Yes, really delete my account</label> + </p> + + <div class="form_submit_buttons"> + <a class="button_action" href="{{ request.urlgen( + 'mediagoblin.user_pages.user_home', + user=user.username) }}">{% trans %}Cancel{% endtrans %}</a> + {{ csrf_token }} + <input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form" /> + </div> + </div> + </form> +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html index 38d99893..cfeb7281 100644 --- a/mediagoblin/templates/mediagoblin/edit/edit_account.html +++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html @@ -53,4 +53,8 @@ </div> </div> </form> + <div class="delete"> + <a href="{{request.urlgen('mediagoblin.edit.delete_account') + }}">Delete my account</a> + </div> {% endblock %} diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py index cbdad649..4bea9243 100644 --- a/mediagoblin/tests/test_edit.py +++ b/mediagoblin/tests/test_edit.py @@ -37,6 +37,24 @@ class TestUserEdit(object): 'password': self.user_password}) + def test_user_deletion(self): + """Delete user via web interface""" + # Make sure user exists + assert User.query.filter_by(username=u'chris').first() + + res = self.app.post('/edit/account/delete/', {'confirmed': 'y'}) + + # Make sure user has been deleted + assert User.query.filter_by(username=u'chris').first() == None + + #TODO: make sure all corresponding items comments etc have been + # deleted too. Perhaps in submission test? + + #Restore user at end of test + self.user = fixture_add_user(password = self.user_password) + self.login() + + def test_change_password(self): """Test changing password correctly and incorrectly""" # test that the password can be changed diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index b9f03e8e..30c78a38 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -23,7 +23,6 @@ from mediagoblin.db.models import (MediaEntry, Collection, CollectionItem, from mediagoblin.tools.response import render_to_response, render_404, redirect from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.pagination import Pagination -from mediagoblin.tools.files import delete_media_files from mediagoblin.user_pages import forms as user_forms from mediagoblin.user_pages.lib import send_comment_email @@ -269,21 +268,7 @@ def media_confirm_delete(request, media): if request.method == 'POST' and form.validate(): if form.confirm.data is True: username = media.get_uploader.username - - # Delete all the associated comments - for comment in media.get_comments(): - comment.delete() - - # Delete all files on the public storage - try: - delete_media_files(media) - except OSError, error: - _log.error('No such files from the user "{1}"' - ' to delete: {0}'.format(str(error), username)) - messages.add_message(request, messages.ERROR, - _('Some of the files with this entry seem' - ' to be missing. Deleting anyway.')) - + # Delete MediaEntry and all related files, comments etc. media.delete() messages.add_message( request, messages.SUCCESS, _('You deleted the media.')) |