Add delete (anonymize) user functionality and a couple other tweaks

This commit is contained in:
Wil Clouser 2010-03-25 17:41:29 -07:00
Родитель 1fd2e81d8e
Коммит 92844214df
9 изменённых файлов: 274 добавлений и 17 удалений

Просмотреть файл

@ -32,6 +32,42 @@ class SetPasswordForm(auth_forms.SetPasswordForm):
super(SetPasswordForm, self).save(**kw)
class UserDeleteForm(forms.Form):
password = forms.CharField(max_length=255, required=True,
widget=forms.PasswordInput(render_value=False))
confirm = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
return super(UserDeleteForm, self).__init__(*args, **kwargs)
def clean_password(self):
data = self.cleaned_data
amouser = self.request.user.get_profile()
if not amouser.check_password(data["password"]):
raise forms.ValidationError(_("Wrong password entered!"))
def clean_confirm(self):
if not self.cleaned_data['confirm']:
msg = _(('You need to check the box "I understand..." before we '
'can delete your account.'))
raise forms.ValidationError(msg)
def clean(self):
amouser = self.request.user.get_profile()
if amouser.is_developer:
# This is tampering because the form isn't shown on the page if the
# user is a developer
log.warning('[Tampering] Attempt to delete developer account (%s)'
% self.request.user)
raise forms.ValidationError("")
def save(self, **kw):
log.info('User (%s) has successfully deleted their account.'
% self.request.user)
super(UserDeleteForm, self).save(**kw)
class UserEditForm(forms.ModelForm):
oldpassword = forms.CharField(max_length=255, required=False,
widget=forms.PasswordInput(render_value=False))
@ -48,12 +84,22 @@ class UserEditForm(forms.ModelForm):
model = models.UserProfile
exclude = ['password']
def clean_nickname(self):
"""We're breaking the rules and allowing null=True and blank=True on a
CharField because I want to enforce uniqueness in the db. In order to
let save() work, I override '' here."""
n = self.cleaned_data['nickname']
if n == '':
n = None
return n
def clean(self):
super(UserEditForm, self).clean()
data = self.cleaned_data
amouser = self.request.user.get_profile()
# Passwords
p1 = data.get("newpassword")
p2 = data.get("newpassword2")
@ -68,6 +114,17 @@ class UserEditForm(forms.ModelForm):
del data["newpassword"]
del data["newpassword2"]
# Names
if not "nickname" in self._errors:
fname = data.get("firstname")
lname = data.get("lastname")
nname = data.get("nickname")
if not (fname or lname or nname):
msg = _("A first name, last name or nickname is required.")
self._errors["firstname"] = ErrorList([msg])
self._errors["lastname"] = ErrorList([msg])
self._errors["nickname"] = ErrorList([msg])
return data
def save(self):

Просмотреть файл

@ -35,9 +35,10 @@ def create_password(algorithm, raw_password):
class UserProfile(amo.models.ModelBase):
nickname = models.CharField(max_length=255, unique=True, default='')
firstname = models.CharField(max_length=255, default='')
lastname = models.CharField(max_length=255, default='')
nickname = models.CharField(max_length=255, unique=True, default='',
null=True, blank=True)
firstname = models.CharField(max_length=255, default='', blank=True)
lastname = models.CharField(max_length=255, default='', blank=True)
password = models.CharField(max_length=255, default='')
email = models.EmailField(unique=True)
@ -77,6 +78,12 @@ class UserProfile(amo.models.ModelBase):
"""public add-ons this user is listed as author of"""
return self.addons.valid().filter(addonuser__listed=True)
@property
def name(self):
"""Can be used while we're transitioning from separate first/last names
to a single field. Bug 546818#6"""
return ('%s %s' % (self.firstname, self.lastname)).strip()
@property
def picture_url(self):
split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id))
@ -115,6 +122,18 @@ class UserProfile(amo.models.ModelBase):
"""All reviews that are not dev replies."""
return self._reviews_all.filter(reply_to=None)
def anonymize(self):
log.info("User (%s: <%s>) is being anonymized." % (self, self.email))
self.email = ""
self.password = "sha512$Anonymous$Password"
self.firstname = ""
self.lastname = ""
self.nickname = ""
self.homepage = ""
self.deleted = True
self.picture_type = ""
self.save()
def save(self, force_insert=False, force_update=False, using=None):
# we have to fix stupid things that we defined poorly in remora
if self.resetcode_expires is None:

Просмотреть файл

@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %}{{ page_title(_('Delete User Account')) }}{% endblock %}
{% block content %}
<div class="primary" role="main">
<div class="primary">
<h2>{{ _('Delete User Account') }}</h2>
{% if messages %}
{% for message in messages %}
<div class="notification-box {{ message.tags }}">
<h2>{{ message }}</h2>
</div>
{% endfor %}
{% endif %}
{% if amouser.is_developer %}
<div class="notification-box info">
{% trans link=url('users.profile', amouser.id) %}
You cannot delete your account if you are listed as an
<a href="{{ link }}"> author of any add-ons</a>. To delete your
account, please have another person in your development group delete
you from the list of authors for your add-ons. Afterwards you will be
able to delete your account here.
{% endtrans %}
</div>
{% else %}
{% if form %}
<div class="notification-box info prose">
<p>{% trans site=settings.DOMAIN %}
By clicking "delete" your account is going to be <strong>permanently removed</strong>. That means:
<ul>
<li>You will not be able to log into {{ site }} anymore.</li>
<li>Your reviews and ratings will not be deleted, but they will no longer be associated with you.</li>
</ul>
{% endtrans %}</p>
</div>
<form method="post" action="">
{{ csrf() }}
<div id="user-edit" class="tab-wrapper">
<div id="user-account" class="tab-panel">
<fieldset>
<h3>{{ _('Confirm account deletion') }}</h3>
<ul>
<label for="id_password">{{ _('Password') }}</label>
{{ form.password|safe }}
{{ form.password.errors|safe }}
</li>
<li>
<label for="id_confirm" class="check">
{{ form.confirm|safe }}
{{ _('I understand this step cannot be undone.') }}
</label>
{{ form.confirm.errors|safe }}
</li>
</ul>
</fieldset>
</div>
<div class="listing-footer">
<button type="submit">{{ _('Delete my user account now') }}</button>
</div>
</div>
</form>
{% endif %}
{% endif %}{# /amouser.is_developer #}
</div>
</div>{# .primary #}
{% endblock content %}

Просмотреть файл

@ -9,7 +9,6 @@
{% block content %}
<div class="primary" role="main">
{# XXX TODO This messages block should be a macro #}
{% if messages %}
{% for message in messages %}
<div class="notification-box {{ message.tags }}">
@ -30,12 +29,16 @@
<h3>{{ _('My account') }}</h3>
<ul>
<li>
<label for="id_lastname">{{ _('Nickname') }}</label>
<label for="id_nickname">
{{ _('Nickname') }} <abbr class="req" title="required">*</abbr>
</label>
{{ form.nickname|safe }}
{{ form.nickname.errors|safe }}
</li>
<li>
<label for="id_email">{{ _('Email Address') }}</label>
<label for="id_email">
{{ _('Email Address') }} <abbr class="req" title="required">*</abbr>
</label>
{{ form.email|safe }}
{{ form.email.errors|safe }}
</li>
@ -176,7 +179,7 @@
</div>{# /#user-profile #}
<div class="listing-footer">
<button type="submit" class="prominent">{{ _('Update') }}</button>
<p id="acct-delete"><a href="#" title="{{ _('Permanently delete your account') }}">{{ _('Delete Account') }}</a></p>
<p id="acct-delete"><a href="{{ url('users.delete') }}" title="{{ _('Permanently delete your account') }}">{{ _('Delete Account') }}</a></p>
</div>
</div>{# /.tab-wrapper #}
</form>

Просмотреть файл

@ -54,8 +54,7 @@
<ul>
{# /users/register #}
<li><a href="#">{{ _("I don't have an account.") }} </a></li>
{# /users/pwreset #}
<li><a href="#">{{ _("I forgot my password.") }}</a></li>
<li><a href="{{ url('users.pwreset') }}">{{ _("I forgot my password.") }}</a></li>
</ul>
</div>
</div>

Просмотреть файл

@ -80,10 +80,81 @@ class TestPasswordResetForm(UserFormBase):
assert mail.outbox[0].body.find('pwreset/%s' % self.uidb36) > 0
class TestUserDeleteForm(UserFormBase):
def test_bad_password(self):
self.client.login(username='jbalogh@mozilla.com', password='foo')
data = {'password': 'wrong', 'confirm': True, }
r = self.client.post('/en-US/firefox/users/delete', data)
msg = "Wrong password entered!"
self.assertFormError(r, 'form', 'password', msg)
def test_not_confirmed(self):
self.client.login(username='jbalogh@mozilla.com', password='foo')
data = {'password': 'foo'}
r = self.client.post('/en-US/firefox/users/delete', data)
msg = ('You need to check the box "I understand..." before we '
'can delete your account.')
self.assertFormError(r, 'form', 'confirm', msg)
def test_success(self):
self.client.login(username='jbalogh@mozilla.com', password='foo')
data = {'password': 'foo', 'confirm': True, }
r = self.client.post('/en-US/firefox/users/delete', data)
self.assertContains(r, "Profile Deleted")
u = User.objects.get(id='4043307').get_profile()
eq_(u.email, '')
class TestUserEditForm(UserFormBase):
def test_set_fail(self):
# 404 right now because log in page doesn't exist
#r = self.client.get('/users/edit', follow=True)
#assert False
pass
def test_no_names(self):
self.client.login(username='jbalogh@mozilla.com', password='foo')
data = {'nickname': '',
'email': 'jbalogh@mozilla.com',
'firstname': '',
'lastname': '', }
r = self.client.post('/en-US/firefox/users/edit', data)
msg = "A first name, last name or nickname is required."
self.assertFormError(r, 'form', 'nickname', msg)
self.assertFormError(r, 'form', 'firstname', msg)
self.assertFormError(r, 'form', 'lastname', msg)
def test_no_real_name(self):
self.client.login(username='jbalogh@mozilla.com', password='foo')
data = {'nickname': 'blah',
'email': 'jbalogh@mozilla.com',
'firstname': '',
'lastname': '', }
r = self.client.post('/en-US/firefox/users/edit', data)
self.assertContains(r, "Profile Updated")
def test_set_wrong_password(self):
self.client.login(username='jbalogh@mozilla.com', password='foo')
data = {'email': 'jbalogh@mozilla.com',
'oldpassword': 'wrong',
'newpassword': 'new',
'newpassword2': 'new', }
r = self.client.post('/en-US/firefox/users/edit', data)
self.assertFormError(r, 'form', 'oldpassword',
'Wrong password entered!')
def test_set_unmatched_passwords(self):
self.client.login(username='jbalogh@mozilla.com', password='foo')
data = {'email': 'jbalogh@mozilla.com',
'oldpassword': 'foo',
'newpassword': 'new1',
'newpassword2': 'new2', }
r = self.client.post('/en-US/firefox/users/edit', data)
self.assertFormError(r, 'form', 'newpassword2',
'The passwords did not match.')
def test_set_new_passwords(self):
self.client.login(username='jbalogh@mozilla.com', password='foo')
data = {'nickname': 'jbalogh',
'email': 'jbalogh@mozilla.com',
'oldpassword': 'foo',
'newpassword': 'new',
'newpassword2': 'new', }
r = self.client.post('/en-US/firefox/users/edit', data)
self.assertContains(r, "Profile Updated")

Просмотреть файл

@ -2,6 +2,7 @@ from datetime import date
import hashlib
from django import test
from django.contrib.auth.models import User
from nose.tools import eq_
@ -13,6 +14,15 @@ from users.models import UserProfile, get_hexdigest
class TestUserProfile(test.TestCase):
fixtures = ['base/addons.json']
fixtures = ['users/test_backends']
def test_anonymize(self):
u = User.objects.get(id='4043307').get_profile()
eq_(u.email, 'jbalogh@mozilla.com')
u.anonymize()
x = User.objects.get(id='4043307').get_profile()
eq_(x.email, "")
def test_display_name_nickname(self):
u = UserProfile(nickname='Terminator', pk=1)
eq_(u.display_name, 'Terminator')
@ -27,6 +37,12 @@ class TestUserProfile(test.TestCase):
eq_(u3.welcome_name, 'sc')
eq_(u4.welcome_name, '')
def test_name(self):
u1 = UserProfile(firstname='Sarah', lastname='Connor')
u2 = UserProfile(firstname='Sarah')
eq_(u1.name, 'Sarah Connor')
eq_(u2.name, 'Sarah') # No trailing space
def test_empty_nickname(self):
u = UserProfile.objects.create(email='yoyoyo@yo.yo', nickname='yoyo')
assert u.user is None

Просмотреть файл

@ -15,11 +15,12 @@ urlpatterns = patterns('',
# URLs for a single user.
('^user/(?P<user_id>\d+)/', include(detail_patterns)),
url('^users/delete$', views.delete, name='users.delete'),
url('^users/edit$', views.edit, name='users.edit'),
url(r'^users/login', views.login, name='users.login'),
url(r'^users/logout', views.logout, name='users.logout'),
url('^users/edit$', views.edit, name='users.edit'),
# Password reset stuff
url(r'^users/pwreset/?$', auth_views.password_reset,
{'template_name': 'pwreset_request.html',

Просмотреть файл

@ -25,6 +25,23 @@ from users.utils import EmailResetCode
log = logging.getLogger('z.users')
@login_required
def delete(request):
amouser = request.user.get_profile()
if request.method == 'POST':
form = forms.UserDeleteForm(request.POST, request=request)
if form.is_valid():
messages.success(request, _('Profile Deleted'))
amouser.anonymize()
logout(request)
form = None
else:
form = forms.UserDeleteForm()
return jingo.render(request, 'delete.html',
{'form': form, 'amouser': amouser})
@login_required
def edit(request):
amouser = request.user.get_profile()
@ -52,7 +69,7 @@ def edit(request):
token, hash = EmailResetCode.create(amouser.id, amouser.email)
url = "%s%s" % (settings.SITE_URL,
reverse('users.emailchange', args=[amouser.id,
token, hash]))
token, hash]))
t = loader.get_template('email/emailchange.ltxt')
c = {'domain': domain, 'url': url, }
send_mail(_(("Please confirm your email address "