aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mediagoblin/db/models.py73
-rw-r--r--mediagoblin/edit/routing.py2
-rw-r--r--mediagoblin/edit/views.py31
-rw-r--r--mediagoblin/static/css/base.css29
-rw-r--r--mediagoblin/static/js/header_dropdown.js27
-rw-r--r--mediagoblin/templates/mediagoblin/base.html52
-rw-r--r--mediagoblin/templates/mediagoblin/edit/delete_account.html43
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit_account.html4
-rw-r--r--mediagoblin/templates/mediagoblin/root.html28
-rw-r--r--mediagoblin/tests/test_edit.py18
-rw-r--r--mediagoblin/user_pages/views.py17
11 files changed, 256 insertions, 68 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..04b4ee28 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -113,10 +113,12 @@ input, textarea {
header {
width: 100%;
+ max-width: 940px;
+ margin-left: auto;
+ margin-right: auto;
padding: 0;
margin-bottom: 42px;
- background-color: #303030;
- border-bottom: 1px solid #252525;
+ border-bottom: 1px solid #333;
}
.header_right {
@@ -125,19 +127,18 @@ header {
float: right;
}
-.header_right ul {
- display: none;
- position: absolute;
- top: 42px;
- right: 0px;
- background: #252525;
- padding: 20px;
+.header_dropdown {
+ margin-bottom: 20px;
}
-.header_right li {
+.header_dropdown li {
list-style: none;
}
+.dropdown_title {
+ font-size: 20px;
+}
+
a.logo {
color: #fff;
font-weight: bold;
@@ -145,7 +146,7 @@ a.logo {
.logo img {
vertical-align: middle;
- margin: 6px 8px;
+ margin: 6px 8px 6px 0;
}
.mediagoblin_content {
@@ -331,6 +332,12 @@ textarea#description, textarea#bio {
height: 100px;
}
+.delete {
+ margin-top: 36px;
+ display: block;
+ text-align: center;
+}
+
/* comments */
.comment_wrapper {
diff --git a/mediagoblin/static/js/header_dropdown.js b/mediagoblin/static/js/header_dropdown.js
new file mode 100644
index 00000000..1b2fb00f
--- /dev/null
+++ b/mediagoblin/static/js/header_dropdown.js
@@ -0,0 +1,27 @@
+/**
+ * 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/>.
+ */
+
+$(document).ready(function(){
+ $(".header_dropdown").hide();
+ $(".header_dropdown_up").hide();
+ $(".header_dropdown_down,.header_dropdown_up").click(function() {
+ $(".header_dropdown_down").toggle();
+ $(".header_dropdown_up").toggle();
+ $(".header_dropdown").slideToggle();
+ });
+});
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index 0a9a56d3..98b32f4a 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -28,6 +28,9 @@
<link rel="shortcut icon"
href="{{ request.staticdirect('/images/goblin.ico') }}" />
<script src="{{ request.staticdirect('/js/extlib/jquery.js') }}"></script>
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/header_dropdown.js') }}"></script>
+
<!--[if lt IE 9]>
<script src="{{ request.staticdirect('/js/extlib/html5shiv.js') }}"></script>
<![endif]-->
@@ -45,21 +48,16 @@
{% block mediagoblin_header_title %}{% endblock %}
<div class="header_right">
{% if request.user %}
- {% trans
- user_url=request.urlgen('mediagoblin.user_pages.user_home',
- user= request.user.username),
- user_name=request.user.username -%}
- <a href="{{ user_url }}">{{ user_name }}</a>'s account
- {%- endtrans %}
- (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>)
{% if request.user and request.user.status == 'active' %}
- <a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">{% trans %}Add media{% endtrans %}</a>
+ <div class="button_action header_dropdown_down">&#9660;</div>
+ <div class="button_action header_dropdown_up">&#9650;</div>
{% elif request.user and request.user.status == "needs_email_verification" %}
{# the following link should only appear when verification is needed #}
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user=request.user.username) }}"
class="button_action_highlight">
{% trans %}Verify your email!{% endtrans %}</a>
+ or <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>
{% endif %}
{% else %}
<a href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{
@@ -68,6 +66,44 @@
{% endif %}
</div>
<div class="clear"></div>
+ {% if request.user and request.user.status == 'active' %}
+ <div class="header_dropdown">
+ <p>
+ <span class="dropdown_title">
+ {% trans user_url=request.urlgen('mediagoblin.user_pages.user_home',
+ user=request.user.username),
+ user_name=request.user.username -%}
+ <a href="{{ user_url }}">{{ user_name }}</a>'s account
+ {%- endtrans %}
+ </span>
+ (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>)
+ </p>
+ <ul>
+ <li><a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">
+ {%- trans %}Add media{% endtrans -%}
+ </a></li>
+ <li><a class="button_action" href="{{ request.urlgen('mediagoblin.submit.collection') }}">
+ {%- trans %}Create new collection{% endtrans -%}
+ </a></li>
+ <li><a href="{{ request.urlgen('mediagoblin.edit.account') }}">
+ {%- trans %}Change account settings{% endtrans -%}
+ </a></li>
+ <li><a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel',
+ user=request.user.username) }}">
+ {%- trans %}Media processing panel{% endtrans -%}
+ </a></li>
+ {% if request.user.is_admin %}
+ <li>Admin:
+ <ul>
+ <li><a href="{{ request.urlgen('mediagoblin.admin.panel') }}">
+ {%- trans %}Media processing panel{% endtrans -%}
+ </a></li>
+ </ul>
+ </li>
+ {% endif %}
+ </ul>
+ </div>
+ {% endif %}
</header>
{% endblock %}
<div class="container">
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/templates/mediagoblin/root.html b/mediagoblin/templates/mediagoblin/root.html
index 11e8f2ac..5c6eb52f 100644
--- a/mediagoblin/templates/mediagoblin/root.html
+++ b/mediagoblin/templates/mediagoblin/root.html
@@ -21,34 +21,6 @@
{% block mediagoblin_content %}
{% if request.user %}
- {% if request.user.status == 'active' %}
- <h1>{% trans %}Actions{% endtrans %}</h1>
- <ul>
- <li><a href="{{ request.urlgen('mediagoblin.submit.start') }}">
- {%- trans %}Add media{% endtrans -%}
- </a></li>
- <li><a href="{{ request.urlgen('mediagoblin.user_pages.collection_list',
- user=request.user.username) }}">
- {%- trans %}Browse collections{% endtrans -%}
- </a></li>
- <li><a href="{{ request.urlgen('mediagoblin.edit.account') }}">
- {%- trans %}Change account settings{% endtrans -%}
- </a></li>
- <li><a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel',
- user=request.user.username) }}">
- {%- trans %}Media processing panel{% endtrans -%}
- </a></li>
- {% if request.user.is_admin %}
- <li>Admin:
- <ul>
- <li><a href="{{ request.urlgen('mediagoblin.admin.panel') }}">
- {%- trans %}Media processing panel{% endtrans -%}
- </a></li>
- </ul>
- </li>
- {% endif %}
- </ul>
- {% endif %}
<h1>{% trans %}Explore{% endtrans %}</h1>
{% else %}
<h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1>
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.'))