зеркало из https://github.com/mozilla/kitsune.git
[617379, 617162] Avatar is replaced when new one is uploaded. Also allow to delete avatar.
* Two new views: edit_avatar, delete_avatar * Each redirect to edit_profile view after success. * Make sure there is always only one avatar file per profile by deleting old ones when new avatars are uploaded.
This commit is contained in:
Родитель
5855049ddf
Коммит
3de879b48f
|
@ -122,11 +122,25 @@ class AuthenticationForm(auth_forms.AuthenticationForm):
|
|||
|
||||
class ProfileForm(forms.ModelForm):
|
||||
"""The form for editing the user's profile."""
|
||||
avatar = forms.ImageField(required=False, widget=ImageWidget)
|
||||
|
||||
class Meta(object):
|
||||
model = Profile
|
||||
exclude = ('user', 'livechat_id')
|
||||
exclude = ('user', 'livechat_id', 'avatar')
|
||||
|
||||
|
||||
class AvatarForm(forms.ModelForm):
|
||||
"""The form for editing the user's avatar."""
|
||||
avatar = forms.ImageField(required=True, widget=ImageWidget)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AvatarForm, self).__init__(*args, **kwargs)
|
||||
self.fields['avatar'].help_text = (
|
||||
u'Your avatar will be resized to {size}x{size}'.format(
|
||||
size=settings.AVATAR_SIZE))
|
||||
|
||||
class Meta(object):
|
||||
model = Profile
|
||||
fields = ('avatar',)
|
||||
|
||||
def clean_avatar(self):
|
||||
if not ('avatar' in self.cleaned_data and self.cleaned_data['avatar']):
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{# vim: set ts=2 et sts=2 sw=2: #}
|
||||
{% extends "users/base.html" %}
|
||||
{% set title = _('Delete avatar') %}
|
||||
{% set classes = 'avatar' %}
|
||||
|
||||
{% block content %}
|
||||
<article id="avatar-delete" class="main">
|
||||
<h1>{{ _('Are you sure you want to delete your avatar?') }}</h1>
|
||||
<div id="avatar-preview">
|
||||
<img src="{{ profile_avatar(profile.user) }}" alt="">
|
||||
</div>
|
||||
|
||||
<form action="{{ url('users.delete_avatar') }}" method="post">
|
||||
{{ csrf() }}
|
||||
<p>
|
||||
{% trans upload_url=url('users.edit_avatar') %}
|
||||
You are about to permanently delete your avatar.
|
||||
<strong>This cannot be undone!</strong>
|
||||
You can always <a href="{{ upload_url }}">upload another avatar</a> to replace your current one.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<div class="form-actions">
|
||||
<a href="{{ url('users.edit_profile') }}">{{ _('Cancel') }}</a>
|
||||
<input type="submit" class="btn g-btn" value="{{ _('Delete avatar') }}" />
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
{# vim: set ts=2 et sts=2 sw=2: #}
|
||||
{% extends "users/base.html" %}
|
||||
{% set title = _('Change Avatar') %}
|
||||
{% set classes = 'profile' %}
|
||||
|
||||
{% block content %}
|
||||
<article id="change-avatar" class="main">
|
||||
<h1>{{ title }}</h1>
|
||||
<form method="post" action="" enctype="multipart/form-data">
|
||||
{{ csrf() }}
|
||||
<ul>
|
||||
{{ form.as_ul()|safe }}
|
||||
</ul>
|
||||
<div class="submit">
|
||||
<input type="submit" value="{{ _('Upload', 'avatar') }}" />
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
{% endblock %}
|
|
@ -6,9 +6,23 @@
|
|||
{% block content %}
|
||||
<article id="edit-profile" class="main">
|
||||
<h1>{{ title }}</h1>
|
||||
<form method="post" action="" enctype="multipart/form-data">
|
||||
<form method="post" action="">
|
||||
{{ csrf() }}
|
||||
<ul>
|
||||
<li id="edit-profile-avatar">
|
||||
<label>{{ _('Avatar') }}</label>
|
||||
<img src="{{ profile_avatar(profile.user) }}">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ url('users.edit_avatar') }}">{{ _('Change', 'avatar') }}</a>
|
||||
</li>
|
||||
{% if profile.avatar %}
|
||||
<li>
|
||||
<a href="{{ url('users.delete_avatar') }}">{{ _('Delete', 'avatar') }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{{ form.as_ul()|safe }}
|
||||
</ul>
|
||||
<div class="submit">
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
from copy import deepcopy
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import mail
|
||||
from django.core.files import File
|
||||
from django.utils.http import int_to_base36
|
||||
|
||||
import mock
|
||||
|
@ -16,6 +18,7 @@ from test_utils import RequestFactory
|
|||
from sumo.urlresolvers import reverse
|
||||
from sumo.helpers import urlparams
|
||||
from sumo.tests import post
|
||||
from users.models import Profile
|
||||
from users.tests import TestCaseBase
|
||||
from users.views import _clean_next_url
|
||||
|
||||
|
@ -214,14 +217,6 @@ class PasswordReset(TestCaseBase):
|
|||
class EditProfileTests(TestCaseBase):
|
||||
fixtures = ['users.json']
|
||||
|
||||
def setUp(self):
|
||||
super(EditProfileTests, self).setUp()
|
||||
self.old_settings = deepcopy(settings._wrapped.__dict__)
|
||||
|
||||
def tearDown(self):
|
||||
settings._wrapped.__dict__ = self.old_settings
|
||||
super(EditProfileTests, self).tearDown()
|
||||
|
||||
def test_edit_profile(self):
|
||||
url = reverse('users.edit_profile')
|
||||
self.client.login(username='rrosario', password='testpass')
|
||||
|
@ -243,9 +238,24 @@ class EditProfileTests(TestCaseBase):
|
|||
eq_(data[key], getattr(profile, key))
|
||||
eq_(data['timezone'], profile.timezone.zone)
|
||||
|
||||
|
||||
class EditAvatarTests(TestCaseBase):
|
||||
fixtures = ['users.json']
|
||||
|
||||
def setUp(self):
|
||||
super(EditAvatarTests, self).setUp()
|
||||
self.old_settings = deepcopy(settings._wrapped.__dict__)
|
||||
|
||||
def tearDown(self):
|
||||
settings._wrapped.__dict__ = self.old_settings
|
||||
user_profile = Profile.objects.get(user__username='rrosario')
|
||||
if user_profile.avatar:
|
||||
user_profile.avatar.delete()
|
||||
super(EditAvatarTests, self).tearDown()
|
||||
|
||||
def test_large_avatar(self):
|
||||
settings.MAX_AVATAR_FILE_SIZE = 1024
|
||||
url = reverse('users.edit_profile')
|
||||
url = reverse('users.edit_avatar')
|
||||
self.client.login(username='rrosario', password='testpass')
|
||||
with open('apps/upload/tests/media/test.jpg') as f:
|
||||
r = self.client.post(url, {'avatar': f})
|
||||
|
@ -254,6 +264,43 @@ class EditProfileTests(TestCaseBase):
|
|||
eq_('"test.jpg" is too large (12KB), the limit is 1KB',
|
||||
doc('.errorlist').text())
|
||||
|
||||
def test_upload_avatar(self):
|
||||
"""Upload a valid avatar."""
|
||||
user_profile = Profile.uncached.get(user__username='rrosario')
|
||||
with open('apps/upload/tests/media/test.jpg') as f:
|
||||
user_profile.avatar.save('test_old.jpg', File(f), save=True)
|
||||
eq_(settings.USER_AVATAR_PATH + 'test_old.jpg',
|
||||
user_profile.avatar.name)
|
||||
old_path = (settings.MEDIA_ROOT + '/' + settings.USER_AVATAR_PATH +
|
||||
'test_old.jpg')
|
||||
assert os.path.exists(old_path), 'Old avatar is not in place.'
|
||||
|
||||
url = reverse('users.edit_avatar')
|
||||
self.client.login(username='rrosario', password='testpass')
|
||||
with open('apps/upload/tests/media/test.jpg') as f:
|
||||
r = self.client.post(url, {'avatar': f})
|
||||
|
||||
user_profile = Profile.uncached.get(user__username='rrosario')
|
||||
eq_(settings.USER_AVATAR_PATH + 'test.jpg', user_profile.avatar.name)
|
||||
eq_(302, r.status_code)
|
||||
eq_('http://testserver/en-US' + reverse('users.edit_profile'),
|
||||
r['location'])
|
||||
assert not os.path.exists(old_path), 'Old avatar was not removed.'
|
||||
|
||||
def test_delete_avatar(self):
|
||||
"""Delete an avatar."""
|
||||
self.test_upload_avatar()
|
||||
|
||||
url = reverse('users.delete_avatar')
|
||||
self.client.login(username='rrosario', password='testpass')
|
||||
r = self.client.post(url)
|
||||
|
||||
user_profile = Profile.objects.get(user__username='rrosario')
|
||||
eq_(302, r.status_code)
|
||||
eq_('http://testserver/en-US' + reverse('users.edit_profile'),
|
||||
r['location'])
|
||||
eq_('', user_profile.avatar.name)
|
||||
|
||||
|
||||
class ViewProfileTests(TestCaseBase):
|
||||
fixtures = ['users.json']
|
||||
|
|
|
@ -17,6 +17,8 @@ users_patterns = patterns('',
|
|||
url(r'^activate/(?P<activation_key>\w+)$', views.activate,
|
||||
name='users.activate'),
|
||||
url(r'^edit$', views.edit_profile, name='users.edit_profile'),
|
||||
url(r'^avatar$', views.edit_avatar, name='users.edit_avatar'),
|
||||
url(r'^avatar/delete$', views.delete_avatar, name='users.delete_avatar'),
|
||||
|
||||
# Password reset
|
||||
url(r'^pwreset$', views.password_reset, name='users.pw_reset'),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -18,7 +19,7 @@ from sumo.decorators import ssl_required, logout_required
|
|||
from sumo.urlresolvers import reverse
|
||||
from upload.tasks import _create_image_thumbnail
|
||||
from users.backends import Sha256Backend # Monkey patch User.set_password.
|
||||
from users.forms import ProfileForm
|
||||
from users.forms import ProfileForm, AvatarForm
|
||||
from users.models import Profile, RegistrationProfile
|
||||
from users.utils import handle_login, handle_register
|
||||
|
||||
|
@ -86,11 +87,6 @@ def edit_profile(request):
|
|||
form = ProfileForm(request.POST, request.FILES, instance=user_profile)
|
||||
if form.is_valid():
|
||||
user_profile = form.save()
|
||||
if user_profile.avatar:
|
||||
content = _create_image_thumbnail(user_profile.avatar.path,
|
||||
settings.AVATAR_SIZE)
|
||||
user_profile.avatar.save(user_profile.avatar.name,
|
||||
content, save=True)
|
||||
return HttpResponseRedirect(reverse('users.profile',
|
||||
args=[request.user.id]))
|
||||
else: # request.method == 'GET'
|
||||
|
@ -103,6 +99,67 @@ def edit_profile(request):
|
|||
{'form': form, 'profile': user_profile})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(['GET', 'POST'])
|
||||
def edit_avatar(request):
|
||||
"""Edit user avatar."""
|
||||
try:
|
||||
user_profile = request.user.get_profile()
|
||||
except Profile.DoesNotExist:
|
||||
# TODO: Once we do user profile migrations, all users should have a
|
||||
# a profile. We can remove this fallback.
|
||||
user_profile = Profile.objects.create(user=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
# Upload new avatar and replace old one.
|
||||
old_avatar_path = None
|
||||
if user_profile.avatar:
|
||||
# Need to store the path, not the file here, or else django's
|
||||
# form.is_valid() messes with it.
|
||||
old_avatar_path = user_profile.avatar.path
|
||||
form = AvatarForm(request.POST, request.FILES, instance=user_profile)
|
||||
if form.is_valid():
|
||||
if old_avatar_path:
|
||||
os.unlink(old_avatar_path)
|
||||
user_profile = form.save()
|
||||
|
||||
content = _create_image_thumbnail(user_profile.avatar.path,
|
||||
settings.AVATAR_SIZE)
|
||||
# Delete uploaded avatar and replace with thumbnail.
|
||||
name = user_profile.avatar.name
|
||||
user_profile.avatar.delete()
|
||||
user_profile.avatar.save(name, content, save=True)
|
||||
return HttpResponseRedirect(reverse('users.edit_profile'))
|
||||
|
||||
else: # request.method == 'GET'
|
||||
form = AvatarForm(instance=user_profile)
|
||||
|
||||
return jingo.render(request, 'users/edit_avatar.html',
|
||||
{'form': form, 'profile': user_profile})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(['GET', 'POST'])
|
||||
def delete_avatar(request):
|
||||
"""Delete user avatar."""
|
||||
try:
|
||||
user_profile = request.user.get_profile()
|
||||
except Profile.DoesNotExist:
|
||||
# TODO: Once we do user profile migrations, all users should have a
|
||||
# a profile. We can remove this fallback.
|
||||
user_profile = Profile.objects.create(user=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
# Delete avatar here
|
||||
if user_profile.avatar:
|
||||
user_profile.avatar.delete()
|
||||
return HttpResponseRedirect(reverse('users.edit_profile'))
|
||||
# else: # request.method == 'GET'
|
||||
|
||||
return jingo.render(request, 'users/confirm_avatar_delete.html',
|
||||
{'profile': user_profile})
|
||||
|
||||
|
||||
def password_reset(request):
|
||||
"""Password reset form.
|
||||
|
||||
|
|
|
@ -67,6 +67,23 @@ article.main div.submit {
|
|||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* edit profile view */
|
||||
#edit-profile-avatar a {
|
||||
padding-left: .5em;
|
||||
}
|
||||
|
||||
#edit-profile-avatar ul {
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
/* edit avatar view */
|
||||
#id_avatar,
|
||||
#change-avatar div.val-wrap {
|
||||
margin-right: 10px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* profile view */
|
||||
#profile section {
|
||||
display: inline-block;
|
||||
|
@ -138,3 +155,20 @@ section#contact-area {
|
|||
#profile p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/* delete avatar */
|
||||
#avatar-delete .form-actions {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#avatar-delete .form-actions input {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#avatar-delete h1 {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#avatar-preview {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче