Commit 7ad4f06d authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

Provide email change mechanism

Refs: #2363
parent fb55b362
......@@ -179,7 +179,6 @@ def get_services(request):
@api_method()
def get_menu(request, with_extra_links=False, with_signout=True):
exclude = []
index_url = reverse('index')
absolute = lambda (url): request.build_absolute_uri(url)
l = [{ 'url': absolute(index_url), 'name': "Sign in"}]
......@@ -199,6 +198,8 @@ def get_menu(request, with_extra_links=False, with_signout=True):
if user.has_usable_password():
l.append({ 'url': absolute(reverse('password_change')),
'name': "Change password" })
l.append({'url':absolute(reverse('email_change')),
'name': "Change email"})
if INVITATIONS_ENABLED:
l.append({ 'url': absolute(reverse('astakos.im.views.invite')),
'name': "Invitations" })
......
......@@ -44,16 +44,20 @@ from django.core.urlresolvers import reverse
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
from django.contrib import messages
from django.utils.encoding import smart_str
from astakos.im.models import AstakosUser, Invitation, get_latest_terms
from astakos.im.models import AstakosUser, Invitation, get_latest_terms, EmailChange
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
from astakos.im.widgets import DummyWidget, RecaptchaWidget
from astakos.im.functions import send_change_email
# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
from astakos.im.util import reverse_lazy, reserved_email, get_query
import logging
import hashlib
import recaptcha.client.captcha as captcha
from random import random
logger = logging.getLogger(__name__)
......@@ -392,6 +396,26 @@ class ExtendedPasswordResetForm(PasswordResetForm):
send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
t.render(Context(c)), from_email, [user.email])
class EmailChangeForm(forms.ModelForm):
class Meta:
model = EmailChange
fields = ('new_email_address',)
def clean_new_email_address(self):
addr = self.cleaned_data['new_email_address']
if AstakosUser.objects.filter(email__iexact=addr):
raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
return addr
def save(self, email_template_name, request, commit=True):
ec = super(EmailChangeForm, self).save(commit=False)
ec.user = request.user
activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address))
ec.activation_key=activation_key.hexdigest()
if commit:
ec.save()
send_change_email(ec, request, email_template_name=email_template_name)
class SignApprovalTermsForm(forms.ModelForm):
class Meta:
model = AstakosUser
......
......@@ -39,6 +39,7 @@ from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError
from django.template import Context, loader
from urllib import quote
from urlparse import urljoin
......@@ -156,6 +157,22 @@ def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
else:
logger.info('Sent feedback from %s', user.email)
def send_change_email(ec, request, email_template_name='registration/email_change_email.txt'):
try:
url = reverse('email_change_confirm',
kwargs={'activation_key':ec.activation_key})
url = request.build_absolute_uri(url)
t = loader.get_template(email_template_name)
c = {'url': url, 'site_name': SITENAME}
from_email = DEFAULT_FROM_EMAIL
send_mail(_("Email change on %s alpha2 testing") % SITENAME,
t.render(Context(c)), from_email, [ec.new_email_address])
except (SMTPException, socket.error) as e:
logger.exception(e)
raise ChangeEmailError()
else:
logger.info('Sent change email for %s', ec.user.email)
def activate(user, email_template_name='im/welcome_email.txt'):
"""
Activates the specific user and sends email.
......@@ -214,4 +231,9 @@ class SendGreetingError(SendMailError):
class SendFeedbackError(SendMailError):
def __init__(self):
self.message = _('Failed to send feedback')
super(SendFeedbackError, self).__init__()
\ No newline at end of file
super(SendFeedbackError, self).__init__()
class ChangeEmailError(SendMailError):
def __init__(self):
self.message = _('Failed to send change email')
super(ChangeEmailError, self).__init__()
\ No newline at end of file
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'EmailChange'
db.create_table('im_emailchange', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('new_email_address', self.gf('django.db.models.fields.EmailField')(max_length=75)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='emailchange_user', unique=True, to=orm['im.AstakosUser'])),
('requested_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 5, 3, 12, 23, 46, 711119))),
('activation_key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=40, db_index=True)),
))
db.send_create_signal('im', ['EmailChange'])
def backwards(self, orm):
# Deleting model 'EmailChange'
db.delete_table('im_emailchange')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'im.approvalterms': {
'Meta': {'object_name': 'ApprovalTerms'},
'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 3, 12, 23, 46, 709576)', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'im.astakosuser': {
'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'invitations': ('django.db.models.fields.IntegerField', [], {'default': '100'}),
'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {}),
'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
},
'im.emailchange': {
'Meta': {'object_name': 'EmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
'requested_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 3, 12, 23, 46, 711119)'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchange_user'", 'unique': 'True', 'to': "orm['im.AstakosUser']"})
},
'im.invitation': {
'Meta': {'object_name': 'Invitation'},
'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
}
}
complete_apps = ['im']
......@@ -42,12 +42,17 @@ from base64 import b64encode
from urlparse import urlparse, urlunparse
from random import randint
from django.db import models
from django.db import models, IntegrityError
from django.contrib.auth.models import User, UserManager, Group
from django.utils.translation import ugettext as _
from django.core.exceptions import ValidationError
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.db import transaction
from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION
from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \
EMAILCHANGE_ACTIVATION_DAYS
QUEUE_CLIENT_ID = 3 # Astakos.
......@@ -270,4 +275,56 @@ def get_latest_terms():
return term
except IndexError:
pass
return None
\ No newline at end of file
return None
class EmailChangeManager(models.Manager):
@transaction.commit_on_success
def change_email(self, activation_key):
"""
Validate an activation key and change the corresponding
``User`` if valid.
If the key is valid and has not expired, return the ``User``
after activating.
If the key is not valid or has expired, return ``None``.
If the key is valid but the ``User`` is already active,
return ``None``.
After successful email change the activation record is deleted.
Throws ValueError if there is already
"""
try:
email_change = self.model.objects.get(activation_key=activation_key)
if email_change.activation_key_expired():
email_change.delete()
raise EmailChange.DoesNotExist
# is there an active user with this address?
try:
AstakosUser.objects.get(email=email_change.new_email_address)
except AstakosUser.DoesNotExist:
pass
else:
raise ValueError(_('The new email address is reserved.'))
# update user
user = AstakosUser.objects.get(pk=email_change.user_id)
user.email = email_change.new_email_address
user.save()
email_change.delete()
return user
except EmailChange.DoesNotExist:
raise ValueError(_('Invalid activation key'))
class EmailChange(models.Model):
new_email_address = models.EmailField(_(u'new e-mail address'), help_text=_(u'Your old email address will be used until you verify your new one.'))
user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
requested_at = models.DateTimeField(default=datetime.now())
activation_key = models.CharField(max_length=40, unique=True, db_index=True)
objects = EmailChangeManager()
def activation_key_expired(self):
expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
return self.requested_at + expiration_date < datetime.now()
\ No newline at end of file
......@@ -83,3 +83,5 @@ PROFILE_EXTRA_LINKS = getattr(settings, 'ASTAKOS_PROFILE_EXTRA_LINKS', {})
# The number of unsuccessful login requests per minute allowed for a specific email
RATELIMIT_RETRIES_ALLOWED = getattr(settings, 'ASTAKOS_RATELIMIT_RETRIES_ALLOWED', 3)
# # Set the expiration time of email change requests
EMAILCHANGE_ACTIVATION_DAYS = getattr(settings, 'ASTAKOS_EMAILCHANGE_ACTIVATION_DAYS', 10)
{% extends 'im/one_col_base.html'%}
{%block page.title %}Email change{% endblock %}
{% block body %}
<div class="section">
<p>Email change sent.</p>
</div>
{% endblock %}
{% extends 'im/one_col_base.html'%}
{%block page.title %}Email change{% endblock %}
{% block body %}
<div class="section">
{% if modified_user %}
<h2>Email changed syccessfully for user {{modified_user.id}}.</h2>
{% endif %}
</div>
{% endblock %}
{% extends "im/email.txt" %}
{% block gr_content %}
Για να ανανεώσετε τον email σας για την υπηρεσία {{ site_name }} της ΕΔΕΤ κατά την alpha2 (δεύτερη δοκιμαστική) φάση λειτουργίας της, χρησιμοποιήστε τον σύνδεσμο: {{url}}
{% endblock %}
{% block gr_note %}{% endblock%}
{% block en_content %}
To change your email for GRNET's {{ site_name }} for its alpha2 testing phase service, you can use the link: {{ url }}.
{% endblock %}
{% block en_note %}{% endblock%}
\ No newline at end of file
{% extends "im/account_base.html" %}
{% block body %}
<form action="{% url astakos.im.views.change_email %}" method="post"
class="withlabels">{% csrf_token %}
{% include "im/form_render.html" %}
<div class="form-row submit">
<input type="hidden" name="next" value="{{ next }}">
<input type="submit" class="submit altcol" value="CHANGE" />
</div>
</form>
{% endblock body %}
......@@ -48,7 +48,10 @@ urlpatterns = patterns('astakos.im.views',
url(r'^activate/?$', 'activate'),
url(r'^approval_terms/?$', 'approval_terms', {}, name='latest_terms'),
url(r'^approval_terms/(?P<term_id>\d+)/?$', 'approval_terms'),
url(r'^password/?$', 'change_password', {}, name='password_change')
url(r'^password/?$', 'change_password', {}, name='password_change'),
url(r'^email_change/?$', 'change_email', {}, name='email_change'),
url(r'^email_change/confirm/(?P<activation_key>\w+)/', 'change_email', {},
name='email_change_confirm')
)
urlpatterns += patterns('astakos.im.target',
......
......@@ -518,3 +518,52 @@ def approval_terms(request, term_id=None, template_name='im/approval_terms.html'
@signed_terms_required
def change_password(request):
return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
@transaction.commit_manually
def change_email(request, activation_key=None,
email_template_name='registration/email_change_email.txt',
form_template_name='registration/email_change_form.html',
confirm_template_name='registration/email_change_done.html',
extra_context={}):
if activation_key:
try:
user = EmailChange.objects.change_email(activation_key)
if request.user.is_authenticated() and request.user == user:
msg = _('Email changed successfully.')
messages.add_message(request, messages.SUCCESS, msg)
auth_logout(request)
response = prepare_response(request, user)
transaction.commit()
return response
except ValueError, e:
messages.add_message(request, messages.ERROR, e)
return render_response(confirm_template_name,
modified_user = user if 'user' in locals() else None,
context_instance = get_context(request,
extra_context))
if not request.user.is_authenticated():
path = quote(request.get_full_path())
url = request.build_absolute_uri(reverse('astakos.im.views.index'))
return HttpResponseRedirect(url + '?next=' + path)
form = EmailChangeForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
try:
ec = form.save(email_template_name, request)
except SendMailError, e:
status = messages.ERROR
msg = e
transaction.rollback()
except IntegrityError, e:
status = messages.ERROR
msg = _('There is already a pending change email request.')
else:
status = messages.SUCCESS
msg = _('Change email request has been registered succefully.\
You are going to receive a verification email in the new address.')
transaction.commit()
messages.add_message(request, status, msg)
return render_response(form_template_name,
form = form,
context_instance = get_context(request,
extra_context))
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment