Add delete (anonymize) user functionality and a couple other tweaks
This commit is contained in:
Родитель
1fd2e81d8e
Коммит
92844214df
|
@ -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 "
|
||||
|
|
Загрузка…
Ссылка в новой задаче