Revision 49790d9d

b/snf-astakos-app/astakos/im/api.py
179 179

  
180 180
@api_method()
181 181
def get_menu(request, with_extra_links=False, with_signout=True):
182
    exclude = []
183 182
    index_url = reverse('index')
184 183
    absolute = lambda (url): request.build_absolute_uri(url)
185 184
    l = [{ 'url': absolute(index_url), 'name': "Sign in"}]
......
199 198
            if user.has_usable_password():
200 199
                l.append({ 'url': absolute(reverse('password_change')),
201 200
                          'name': "Change password" })
201
            l.append({'url':absolute(reverse('email_change')),
202
                      'name': "Change email"})
202 203
            if INVITATIONS_ENABLED:
203 204
                l.append({ 'url': absolute(reverse('astakos.im.views.invite')),
204 205
                          'name': "Invitations" })
b/snf-astakos-app/astakos/im/forms.py
44 44
from django.utils.functional import lazy
45 45
from django.utils.safestring import mark_safe
46 46
from django.contrib import messages
47
from django.utils.encoding import smart_str
47 48

  
48
from astakos.im.models import AstakosUser, Invitation, get_latest_terms
49
from astakos.im.models import AstakosUser, Invitation, get_latest_terms, EmailChange
49 50
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
50 51
from astakos.im.widgets import DummyWidget, RecaptchaWidget
52
from astakos.im.functions import send_change_email
51 53

  
52 54
# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
53 55
from astakos.im.util import reverse_lazy, reserved_email, get_query
54 56

  
55 57
import logging
58
import hashlib
56 59
import recaptcha.client.captcha as captcha
60
from random import random
57 61

  
58 62
logger = logging.getLogger(__name__)
59 63

  
......
392 396
            send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
393 397
                t.render(Context(c)), from_email, [user.email])
394 398

  
399
class EmailChangeForm(forms.ModelForm):
400
    class Meta:
401
        model = EmailChange
402
        fields = ('new_email_address',)
403
            
404
    def clean_new_email_address(self):
405
        addr = self.cleaned_data['new_email_address']
406
        if AstakosUser.objects.filter(email__iexact=addr):
407
            raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
408
        return addr
409
    
410
    def save(self, email_template_name, request, commit=True):
411
        ec = super(EmailChangeForm, self).save(commit=False)
412
        ec.user = request.user
413
        activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address))
414
        ec.activation_key=activation_key.hexdigest()
415
        if commit:
416
            ec.save()
417
        send_change_email(ec, request, email_template_name=email_template_name)
418

  
395 419
class SignApprovalTermsForm(forms.ModelForm):
396 420
    class Meta:
397 421
        model = AstakosUser
b/snf-astakos-app/astakos/im/functions.py
39 39
from django.core.mail import send_mail
40 40
from django.core.urlresolvers import reverse
41 41
from django.core.exceptions import ValidationError
42
from django.template import Context, loader
42 43

  
43 44
from urllib import quote
44 45
from urlparse import urljoin
......
156 157
    else:
157 158
        logger.info('Sent feedback from %s', user.email)
158 159

  
160
def send_change_email(ec, request, email_template_name='registration/email_change_email.txt'):
161
    try:
162
        url = reverse('email_change_confirm',
163
                      kwargs={'activation_key':ec.activation_key})
164
        url = request.build_absolute_uri(url)
165
        t = loader.get_template(email_template_name)
166
        c = {'url': url, 'site_name': SITENAME}
167
        from_email = DEFAULT_FROM_EMAIL
168
        send_mail(_("Email change on %s alpha2 testing") % SITENAME,
169
            t.render(Context(c)), from_email, [ec.new_email_address])
170
    except (SMTPException, socket.error) as e:
171
        logger.exception(e)
172
        raise ChangeEmailError()
173
    else:
174
        logger.info('Sent change email for %s', ec.user.email)
175

  
159 176
def activate(user, email_template_name='im/welcome_email.txt'):
160 177
    """
161 178
    Activates the specific user and sends email.
......
214 231
class SendFeedbackError(SendMailError):
215 232
    def __init__(self):
216 233
        self.message = _('Failed to send feedback')
217
        super(SendFeedbackError, self).__init__()
234
        super(SendFeedbackError, self).__init__()
235

  
236
class ChangeEmailError(SendMailError):
237
    def __init__(self):
238
        self.message = _('Failed to send change email')
239
        super(ChangeEmailError, self).__init__()
b/snf-astakos-app/astakos/im/migrations/0008_auto__add_emailchange.py
1
# encoding: utf-8
2
import datetime
3
from south.db import db
4
from south.v2 import SchemaMigration
5
from django.db import models
6

  
7
class Migration(SchemaMigration):
8

  
9
    def forwards(self, orm):
10
        
11
        # Adding model 'EmailChange'
12
        db.create_table('im_emailchange', (
13
            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14
            ('new_email_address', self.gf('django.db.models.fields.EmailField')(max_length=75)),
15
            ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='emailchange_user', unique=True, to=orm['im.AstakosUser'])),
16
            ('requested_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 5, 3, 12, 23, 46, 711119))),
17
            ('activation_key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=40, db_index=True)),
18
        ))
19
        db.send_create_signal('im', ['EmailChange'])
20

  
21

  
22
    def backwards(self, orm):
23
        
24
        # Deleting model 'EmailChange'
25
        db.delete_table('im_emailchange')
26

  
27

  
28
    models = {
29
        'auth.group': {
30
            'Meta': {'object_name': 'Group'},
31
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
32
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
33
            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
34
        },
35
        'auth.permission': {
36
            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
37
            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
38
            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
39
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
40
            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
41
        },
42
        'auth.user': {
43
            'Meta': {'object_name': 'User'},
44
            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
45
            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
46
            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
47
            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
48
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
49
            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
50
            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
51
            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
52
            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
53
            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
54
            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
55
            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
56
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
57
        },
58
        'contenttypes.contenttype': {
59
            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
60
            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
61
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62
            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
63
            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
64
        },
65
        'im.approvalterms': {
66
            'Meta': {'object_name': 'ApprovalTerms'},
67
            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 3, 12, 23, 46, 709576)', 'db_index': 'True'}),
68
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
69
            'location': ('django.db.models.fields.CharField', [], {'max_length': '255'})
70
        },
71
        'im.astakosuser': {
72
            'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
73
            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
74
            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
75
            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
76
            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
77
            'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
78
            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
79
            'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
80
            'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
81
            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '100'}),
82
            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
83
            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
84
            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
85
            'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
86
            'updated': ('django.db.models.fields.DateTimeField', [], {}),
87
            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
88
        },
89
        'im.emailchange': {
90
            'Meta': {'object_name': 'EmailChange'},
91
            'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}),
92
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
93
            'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
94
            'requested_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 3, 12, 23, 46, 711119)'}),
95
            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchange_user'", 'unique': 'True', 'to': "orm['im.AstakosUser']"})
96
        },
97
        'im.invitation': {
98
            'Meta': {'object_name': 'Invitation'},
99
            'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
100
            'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
101
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
102
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
103
            'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
104
            'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
105
            'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
106
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
107
        }
108
    }
109

  
110
    complete_apps = ['im']
b/snf-astakos-app/astakos/im/models.py
42 42
from urlparse import urlparse, urlunparse
43 43
from random import randint
44 44

  
45
from django.db import models
45
from django.db import models, IntegrityError
46 46
from django.contrib.auth.models import User, UserManager, Group
47 47
from django.utils.translation import ugettext as _
48 48
from django.core.exceptions import ValidationError
49
from django.template.loader import render_to_string
50
from django.core.mail import send_mail
51
from django.db import transaction
49 52

  
50
from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION
53
from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
54
AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \
55
EMAILCHANGE_ACTIVATION_DAYS
51 56

  
52 57
QUEUE_CLIENT_ID = 3 # Astakos.
53 58

  
......
270 275
        return term
271 276
    except IndexError:
272 277
        pass
273
    return None
278
    return None
279

  
280
class EmailChangeManager(models.Manager):
281
    @transaction.commit_on_success
282
    def change_email(self, activation_key):
283
        """
284
        Validate an activation key and change the corresponding
285
        ``User`` if valid.
286

  
287
        If the key is valid and has not expired, return the ``User``
288
        after activating.
289

  
290
        If the key is not valid or has expired, return ``None``.
291

  
292
        If the key is valid but the ``User`` is already active,
293
        return ``None``.
294

  
295
        After successful email change the activation record is deleted.
296

  
297
        Throws ValueError if there is already
298
        """
299
        try:
300
            email_change = self.model.objects.get(activation_key=activation_key)
301
            if email_change.activation_key_expired():
302
                email_change.delete()
303
                raise EmailChange.DoesNotExist
304
            # is there an active user with this address?
305
            try:
306
                AstakosUser.objects.get(email=email_change.new_email_address)
307
            except AstakosUser.DoesNotExist:
308
                pass
309
            else:
310
                raise ValueError(_('The new email address is reserved.'))
311
            # update user
312
            user = AstakosUser.objects.get(pk=email_change.user_id)
313
            user.email = email_change.new_email_address
314
            user.save()
315
            email_change.delete()
316
            return user
317
        except EmailChange.DoesNotExist:
318
            raise ValueError(_('Invalid activation key'))
319

  
320
class EmailChange(models.Model):
321
    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.'))
322
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
323
    requested_at = models.DateTimeField(default=datetime.now())
324
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
325

  
326
    objects = EmailChangeManager()
327

  
328
    def activation_key_expired(self):
329
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
330
        return self.requested_at + expiration_date < datetime.now()
b/snf-astakos-app/astakos/im/settings.py
83 83
# The number of unsuccessful login requests per minute allowed for a specific email
84 84
RATELIMIT_RETRIES_ALLOWED = getattr(settings, 'ASTAKOS_RATELIMIT_RETRIES_ALLOWED', 3)
85 85

  
86
# # Set the expiration time of email change requests
87
EMAILCHANGE_ACTIVATION_DAYS = getattr(settings, 'ASTAKOS_EMAILCHANGE_ACTIVATION_DAYS', 10)
b/snf-astakos-app/astakos/im/templates/registration/email_change_confirm.html
1
{% extends 'im/one_col_base.html'%}
2

  
3
{%block page.title %}Email change{% endblock %}
4
{% block body %}
5
<div class="section">
6
    <p>Email change sent.</p>
7
</div>
8
{% endblock %}
b/snf-astakos-app/astakos/im/templates/registration/email_change_done.html
1
{% extends 'im/one_col_base.html'%}
2

  
3
{%block page.title %}Email change{% endblock %}
4
{% block body %}
5
<div class="section">
6
    {% if modified_user %}
7
    <h2>Email changed syccessfully for user {{modified_user.id}}.</h2>
8
    {% endif %}
9
</div>
10
{% endblock %}
b/snf-astakos-app/astakos/im/templates/registration/email_change_email.txt
1
{% extends "im/email.txt" %}
2

  
3
{% block gr_content %}
4
Για να ανανεώσετε τον email σας για την υπηρεσία {{ site_name }} της ΕΔΕΤ κατά την alpha2 (δεύτερη δοκιμαστική) φάση λειτουργίας της, χρησιμοποιήστε τον σύνδεσμο: {{url}}
5
{% endblock %}
6

  
7
{% block gr_note %}{% endblock%}
8

  
9
{% block en_content %}
10
To change your email for GRNET's {{ site_name }} for its alpha2 testing phase service, you can use the  link: {{ url }}.
11
{% endblock %}
12

  
13
{% block en_note %}{% endblock%}
b/snf-astakos-app/astakos/im/templates/registration/email_change_form.html
1
{% extends "im/account_base.html" %}
2

  
3
{% block body %}
4
<form action="{% url astakos.im.views.change_email %}" method="post"
5
    class="withlabels">{% csrf_token %}
6

  
7
    {% include "im/form_render.html" %}
8

  
9
    <div class="form-row submit">
10
        <input type="hidden" name="next" value="{{ next }}">
11
        <input type="submit" class="submit altcol" value="CHANGE" />
12
    </div>
13
</form>
14
{% endblock body %}
b/snf-astakos-app/astakos/im/urls.py
48 48
    url(r'^activate/?$', 'activate'),
49 49
    url(r'^approval_terms/?$', 'approval_terms', {}, name='latest_terms'),
50 50
    url(r'^approval_terms/(?P<term_id>\d+)/?$', 'approval_terms'),
51
    url(r'^password/?$', 'change_password', {}, name='password_change')
51
    url(r'^password/?$', 'change_password', {}, name='password_change'),
52
    url(r'^email_change/?$', 'change_email', {}, name='email_change'),
53
    url(r'^email_change/confirm/(?P<activation_key>\w+)/', 'change_email', {},
54
        name='email_change_confirm')
52 55
)
53 56

  
54 57
urlpatterns += patterns('astakos.im.target',
b/snf-astakos-app/astakos/im/views.py
518 518
@signed_terms_required
519 519
def change_password(request):
520 520
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
521

  
522
@transaction.commit_manually
523
def change_email(request, activation_key=None,
524
                 email_template_name='registration/email_change_email.txt',
525
                 form_template_name='registration/email_change_form.html',
526
                 confirm_template_name='registration/email_change_done.html',
527
                 extra_context={}):
528
    if activation_key:
529
        try:
530
            user = EmailChange.objects.change_email(activation_key)
531
            if request.user.is_authenticated() and request.user == user:
532
                msg = _('Email changed successfully.')
533
                messages.add_message(request, messages.SUCCESS, msg)
534
                auth_logout(request)
535
                response = prepare_response(request, user)
536
                transaction.commit()
537
                return response
538
        except ValueError, e:
539
            messages.add_message(request, messages.ERROR, e)
540
        return render_response(confirm_template_name,
541
                               modified_user = user if 'user' in locals() else None,
542
                               context_instance = get_context(request,
543
                                                              extra_context))
544
    
545
    if not request.user.is_authenticated():
546
        path = quote(request.get_full_path())
547
        url = request.build_absolute_uri(reverse('astakos.im.views.index'))
548
        return HttpResponseRedirect(url + '?next=' + path)
549
    form = EmailChangeForm(request.POST or None)
550
    if request.method == 'POST' and form.is_valid():
551
        try:
552
            ec = form.save(email_template_name, request)
553
        except SendMailError, e:
554
            status = messages.ERROR
555
            msg = e
556
            transaction.rollback()
557
        except IntegrityError, e:
558
            status = messages.ERROR
559
            msg = _('There is already a pending change email request.')
560
        else:
561
            status = messages.SUCCESS
562
            msg = _('Change email request has been registered succefully.\
563
                    You are going to receive a verification email in the new address.')
564
            transaction.commit()
565
        messages.add_message(request, status, msg)
566
    return render_response(form_template_name,
567
                           form = form,
568
                           context_instance = get_context(request,
569
                                                          extra_context))

Also available in: Unified diff