[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:
Paul Craciunoiu 2010-12-07 14:23:36 -08:00
Родитель 5855049ddf
Коммит 3de879b48f
8 изменённых файлов: 233 добавлений и 18 удалений

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

@ -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;
}