Revision 8316698a

b/snf-astakos-app/README
70 70
                                                                                                                    (see: http://code.google.com/intl/el-GR/apis/recaptcha/docs/customization.html)
71 71
ASTAKOS_LOGOUT_NEXT                                                                                                 Where the user should be redirected after logout
72 72
                                                                                                                    (if not set and no next parameter is defined it renders login page with message)
73
ASTAKOS_RE_USER_EMAIL_PATTERNS      []                                                                              Email patterns that are automatically activated ex. ['^[a-zA-Z0-9\._-]+@grnet\.gr$']
73 74
==============================      =============================================================================   ===========================================================================================
74 75

  
75 76
Administrator functions
b/snf-astakos-app/astakos/im/backends.py
48 48
from astakos.im.models import AstakosUser, Invitation
49 49
from astakos.im.forms import *
50 50
from astakos.im.util import get_invitation
51
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL
51
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
52 52

  
53 53
import socket
54 54
import logging
55
import re
55 56

  
56 57
logger = logging.getLogger(__name__)
57 58

  
......
78 79
        raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
79 80
    return backend_class(request)
80 81

  
81
class InvitationsBackend(object):
82
class SignupBackend(object):
83
    def _is_preaccepted(self, user):
84
        # return True if user email matches specific patterns
85
        for pattern in RE_USER_EMAIL_PATTERNS:
86
            if re.match(pattern, user.email):
87
                return True
88
        return False
89

  
90
class InvitationsBackend(SignupBackend):
82 91
    """
83 92
    A registration backend which implements the following workflow: a user
84 93
    supplies the necessary registation information, if the request contains a valid
......
93 102
        """
94 103
        self.request = request
95 104
        self.invitation = get_invitation(request)
105
        super(InvitationsBackend, self).__init__()
96 106

  
97 107
    def get_signup_form(self, provider):
98 108
        """
......
136 146
        If there is a valid, not-consumed invitation code for the specific user
137 147
        returns True else returns False.
138 148
        """
149
        if super(InvitationsBackend, self)._is_preaccepted(user):
150
            return True
139 151
        invitation = self.invitation
140 152
        if not invitation:
141 153
            return False
......
145 157
        return False
146 158

  
147 159
    @transaction.commit_manually
148
    def signup(self, form, admin_email_template_name='im/admin_notification.txt'):
160
    def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
149 161
        """
150 162
        Initially creates an inactive user account. If the user is preaccepted
151 163
        (has a valid invitation code) the user is activated and if the request
......
159 171
        try:
160 172
            user = form.save()
161 173
            if self._is_preaccepted(user):
162
                user.is_active = True
163
                user.save()
164
                message = _('Registration completed. You can now login.')
174
                if user.email_verified:
175
                    user.is_active = True
176
                    user.save()
177
                    message = _('Registration completed. You can now login.')
178
                else:
179
                    try:
180
                        _send_verification(self.request, user, email_template_name)
181
                        message = _('Verification sent to %s' % user.email)
182
                    except (SMTPException, socket.error) as e:
183
                        status = messages.ERROR
184
                        name = 'strerror'
185
                        message = getattr(e, name) if hasattr(e, name) else e
165 186
            else:
166 187
                _send_notification(user, admin_email_template_name)
167 188
                message = _('Your request for an account was successfully sent \
......
183 204
            transaction.commit()
184 205
        return status, message, user
185 206

  
186
class SimpleBackend(object):
207
class SimpleBackend(SignupBackend):
187 208
    """
188 209
    A registration backend which implements the following workflow: a user
189 210
    supplies the necessary registation information, an incative user account is
......
191 212
    """
192 213
    def __init__(self, request):
193 214
        self.request = request
215
        super(SimpleBackend, self).__init__()
194 216

  
195 217
    def get_signup_form(self, provider):
196 218
        """
......
207 229
        ip = self.request.META.get('REMOTE_ADDR',
208 230
                self.request.META.get('HTTP_X_REAL_IP', None))
209 231
        return globals()[formclass](initial_data, ip=ip)
210

  
232
    
233
    def _is_preaccepted(self, user):
234
        if super(SimpleBackend, self)._is_preaccepted(user):
235
            return True
236
        if MODERATION_ENABLED:
237
            return False
238
        return True
239
    
211 240
    @transaction.commit_manually
212 241
    def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
213 242
        """
......
233 262
        """
234 263
        user = form.save()
235 264
        status = messages.SUCCESS
236
        if MODERATION_ENABLED:
265
        if not self._is_preaccepted(user):
237 266
            try:
238 267
                _send_notification(user, admin_email_template_name)
239 268
                message = _('Your request for an account was successfully sent \
b/snf-astakos-app/astakos/im/forms.py
54 54
    """
55 55
    Extends the built in UserCreationForm in several ways:
56 56
    
57
    * Adds email, first_name and last_name field.
57
    * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
58 58
    * The username field isn't visible and it is assigned a generated id.
59 59
    * User created is not active. 
60 60
    """
......
134 134
        """
135 135
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
136 136
        self.fields.keyOrder = ['email', 'inviter', 'first_name',
137
                                'last_name', 'password1', 'password2']
137
                                'last_name', 'password1', 'password2',
138
                                'recaptcha_challenge_field',
139
                                'recaptcha_response_field']
138 140
        #set readonly form fields
139 141
        self.fields['inviter'].widget.attrs['readonly'] = True
140 142
        self.fields['email'].widget.attrs['readonly'] = True
......
144 146
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
145 147
        level = user.invitation.inviter.level + 1
146 148
        user.level = level
147
        user.invitations = INVITATIONS_PER_LEVEL[level]
149
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
150
        user.email_verified = True
148 151
        if commit:
149 152
            user.save()
150 153
        return user
b/snf-astakos-app/astakos/im/management/commands/inviteuser.py
36 36
from smtplib import SMTPException
37 37

  
38 38
from django.core.management.base import BaseCommand, CommandError
39
from django.db.utils import IntegrityError
39 40

  
40 41
from astakos.im.functions import invite
41 42

  
......
63 64
                self.stdout.write("Invitation sent to '%s'\n" % (email,))
64 65
            except (SMTPException, socket.error) as e:
65 66
                raise CommandError("Error sending the invitation")
67
            except IntegrityError, e:
68
                raise CommandError("There is already an invitation for %s" % (email,))
66 69
        else:
67 70
            raise CommandError("No invitations left")
b/snf-astakos-app/astakos/im/management/commands/modifyuser.py
47 47
            dest='invitations',
48 48
            metavar='NUM',
49 49
            help="Update user's invitations"),
50
        make_option('--level',
51
            dest='level',
52
            metavar='NUM',
53
            help="Update user's level"),
50 54
        make_option('--password',
51 55
            dest='password',
52 56
            metavar='PASSWORD',
......
100 104
        if invitations is not None:
101 105
            user.invitations = int(invitations)
102 106
        
107
        level = options.get('level')
108
        if level is not None:
109
            user.level = int(level)
110
        
103 111
        password = options.get('password')
104 112
        if password is not None:
105 113
            user.set_password(password)
b/snf-astakos-app/astakos/im/migrations/0003_auto__add_unique_invitation_username.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 unique constraint on 'Invitation', fields ['username']
12
        db.create_unique('im_invitation', ['username'])
13

  
14

  
15
    def backwards(self, orm):
16
        
17
        # Removing unique constraint on 'Invitation', fields ['username']
18
        db.delete_unique('im_invitation', ['username'])
19

  
20

  
21
    models = {
22
        'auth.group': {
23
            'Meta': {'object_name': 'Group'},
24
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
25
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
26
            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
27
        },
28
        'auth.permission': {
29
            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
30
            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
31
            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
32
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
33
            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
34
        },
35
        'auth.user': {
36
            'Meta': {'object_name': 'User'},
37
            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
38
            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
39
            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
40
            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
41
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
42
            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
43
            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
44
            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
45
            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
46
            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
47
            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
48
            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
49
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
50
        },
51
        'contenttypes.contenttype': {
52
            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
53
            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
54
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55
            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
56
            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
57
        },
58
        'im.astakosuser': {
59
            'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
60
            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
61
            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
62
            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
63
            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
64
            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
65
            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
66
            'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}),
67
            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
68
            'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
69
            'updated': ('django.db.models.fields.DateTimeField', [], {}),
70
            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
71
        },
72
        'im.invitation': {
73
            'Meta': {'object_name': 'Invitation'},
74
            'accepted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
75
            'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
76
            'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
77
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
78
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79
            'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
80
            'is_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
81
            'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
82
            'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
83
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
84
        }
85
    }
86

  
87
    complete_apps = ['im']
b/snf-astakos-app/astakos/im/migrations/0004_auto__add_field_astakosuser_email_verified.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 field 'AstakosUser.email_verified'
12
        db.add_column('im_astakosuser', 'email_verified', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
13

  
14

  
15
    def backwards(self, orm):
16
        
17
        # Deleting field 'AstakosUser.email_verified'
18
        db.delete_column('im_astakosuser', 'email_verified')
19

  
20

  
21
    models = {
22
        'auth.group': {
23
            'Meta': {'object_name': 'Group'},
24
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
25
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
26
            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
27
        },
28
        'auth.permission': {
29
            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
30
            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
31
            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
32
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
33
            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
34
        },
35
        'auth.user': {
36
            'Meta': {'object_name': 'User'},
37
            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
38
            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
39
            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
40
            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
41
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
42
            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
43
            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
44
            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
45
            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
46
            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
47
            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
48
            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
49
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
50
        },
51
        'contenttypes.contenttype': {
52
            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
53
            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
54
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55
            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
56
            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
57
        },
58
        'im.astakosuser': {
59
            'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
60
            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
61
            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
62
            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
63
            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
64
            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
65
            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
66
            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
67
            'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}),
68
            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
69
            'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
70
            'updated': ('django.db.models.fields.DateTimeField', [], {}),
71
            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
72
        },
73
        'im.invitation': {
74
            'Meta': {'object_name': 'Invitation'},
75
            'accepted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
76
            'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
77
            'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
78
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
79
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
80
            'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
81
            'is_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
82
            'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
83
            'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
84
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
85
        }
86
    }
87

  
88
    complete_apps = ['im']
b/snf-astakos-app/astakos/im/models.py
56 56
    #for invitations
57 57
    user_level = DEFAULT_USER_LEVEL
58 58
    level = models.IntegerField('Inviter level', default=user_level)
59
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL[user_level])
59
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
60 60
    
61 61
    auth_token = models.CharField('Authentication Token', max_length=32,
62 62
                                  null=True, blank=True)
......
69 69
    # ex. screen_name for twitter, eppn for shibboleth
70 70
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
71 71
    
72
    email_verified = models.BooleanField('Email verified?', default=False)
73
    
72 74
    @property
73 75
    def realname(self):
74 76
        return '%s %s' %(self.first_name, self.last_name)
......
127 129
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
128 130
                                null=True)
129 131
    realname = models.CharField('Real name', max_length=255)
130
    username = models.CharField('Unique ID', max_length=255)
132
    username = models.CharField('Unique ID', max_length=255, unique=True)
131 133
    code = models.BigIntegerField('Invitation code', db_index=True)
132 134
    #obsolete: we keep it just for transfering the data
133 135
    is_accepted = models.BooleanField('Accepted?', default=False)
b/snf-astakos-app/astakos/im/settings.py
61 61
# Set where the user should be redirected after logout
62 62
LOGOUT_NEXT = getattr(settings, 'ASTAKOS_LOGOUT_NEXT', '')
63 63

  
64
# Set user email patterns that are automatically activated
65
RE_USER_EMAIL_PATTERNS = getattr(settings, 'ASTAKOS_RE_USER_EMAIL_PATTERNS', [])
b/snf-astakos-app/astakos/im/util.py
75 75
            'password':password,
76 76
            'affiliation':affiliation,
77 77
            'level':level,
78
            'invitations':INVITATIONS_PER_LEVEL[level],
78
            'invitations':INVITATIONS_PER_LEVEL.get(level, 0),
79 79
            'provider':provider,
80 80
            'realname':realname,
81 81
            'first_name':first_name,
b/snf-astakos-app/astakos/im/views.py
50 50
from django.contrib.auth import logout as auth_logout
51 51
from django.utils.http import urlencode
52 52
from django.http import HttpResponseRedirect
53
from django.db.utils import IntegrityError
53 54

  
54 55
from astakos.im.models import AstakosUser, Invitation
55 56
from astakos.im.backends import get_backend
......
177 178
                status = messages.ERROR
178 179
                message = getattr(e, 'strerror', '')
179 180
                transaction.rollback()
181
            except IntegrityError, e:
182
                status = messages.ERROR
183
                message = _('There is already invitation for %s' % username)
184
                transaction.rollback()
180 185
        else:
181 186
            status = messages.ERROR
182 187
            message = _('No invitations left')
......
261 266
    Upon successful user creation if ``next`` url parameter is present the user is redirected there
262 267
    otherwise renders the same page with a success message.
263 268
    
264
    On unsuccessful creation, renders the same page with an error message.
269
    On unsuccessful creation, renders ``on_failure`` with an error message.
265 270
    
266 271
    **Arguments**
267 272
    
......
405 410
        return HttpResponseBadRequest('No such user')
406 411
    
407 412
    user.is_active = True
413
    user.email_verified = True
408 414
    user.save()
409 415
    return prepare_response(request, user, next, renew=True)

Also available in: Unified diff