Automatically activate users whose email matches specific email patterns defined...
authorSofia Papagiannaki <papagian@gmail.com>
Wed, 7 Mar 2012 16:02:48 +0000 (18:02 +0200)
committerSofia Papagiannaki <papagian@gmail.com>
Wed, 7 Mar 2012 16:02:48 +0000 (18:02 +0200)
* introduce email_verified AstakosUser field and if False during signup send email verification
* show recaptcha fields in signup form in case of invited user
* do not allow multiple invitations with the same receiver
* enable user level modification by ``snf-manage modifyuser``

Refs: #2166

snf-astakos-app/README
snf-astakos-app/astakos/im/backends.py
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/management/commands/inviteuser.py
snf-astakos-app/astakos/im/management/commands/modifyuser.py
snf-astakos-app/astakos/im/migrations/0003_auto__add_unique_invitation_username.py [new file with mode: 0644]
snf-astakos-app/astakos/im/migrations/0004_auto__add_field_astakosuser_email_verified.py [new file with mode: 0644]
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/settings.py
snf-astakos-app/astakos/im/util.py
snf-astakos-app/astakos/im/views.py

index f6927bf..18b0b1f 100644 (file)
@@ -70,6 +70,7 @@ ASTAKOS_RECAPTCHA_OPTIONS           {'theme': 'white'}
                                                                                                                     (see: http://code.google.com/intl/el-GR/apis/recaptcha/docs/customization.html)
 ASTAKOS_LOGOUT_NEXT                                                                                                 Where the user should be redirected after logout
                                                                                                                     (if not set and no next parameter is defined it renders login page with message)
+ASTAKOS_RE_USER_EMAIL_PATTERNS      []                                                                              Email patterns that are automatically activated ex. ['^[a-zA-Z0-9\._-]+@grnet\.gr$']
 ==============================      =============================================================================   ===========================================================================================
 
 Administrator functions
index 12e7037..9bcaf54 100644 (file)
@@ -48,10 +48,11 @@ from urlparse import urljoin
 from astakos.im.models import AstakosUser, Invitation
 from astakos.im.forms import *
 from astakos.im.util import get_invitation
-from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL
+from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
 
 import socket
 import logging
+import re
 
 logger = logging.getLogger(__name__)
 
@@ -78,7 +79,15 @@ def get_backend(request):
         raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
     return backend_class(request)
 
-class InvitationsBackend(object):
+class SignupBackend(object):
+    def _is_preaccepted(self, user):
+        # return True if user email matches specific patterns
+        for pattern in RE_USER_EMAIL_PATTERNS:
+            if re.match(pattern, user.email):
+                return True
+        return False
+
+class InvitationsBackend(SignupBackend):
     """
     A registration backend which implements the following workflow: a user
     supplies the necessary registation information, if the request contains a valid
@@ -93,6 +102,7 @@ class InvitationsBackend(object):
         """
         self.request = request
         self.invitation = get_invitation(request)
+        super(InvitationsBackend, self).__init__()
 
     def get_signup_form(self, provider):
         """
@@ -136,6 +146,8 @@ class InvitationsBackend(object):
         If there is a valid, not-consumed invitation code for the specific user
         returns True else returns False.
         """
+        if super(InvitationsBackend, self)._is_preaccepted(user):
+            return True
         invitation = self.invitation
         if not invitation:
             return False
@@ -145,7 +157,7 @@ class InvitationsBackend(object):
         return False
 
     @transaction.commit_manually
-    def signup(self, form, admin_email_template_name='im/admin_notification.txt'):
+    def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
         """
         Initially creates an inactive user account. If the user is preaccepted
         (has a valid invitation code) the user is activated and if the request
@@ -159,9 +171,18 @@ class InvitationsBackend(object):
         try:
             user = form.save()
             if self._is_preaccepted(user):
-                user.is_active = True
-                user.save()
-                message = _('Registration completed. You can now login.')
+                if user.email_verified:
+                    user.is_active = True
+                    user.save()
+                    message = _('Registration completed. You can now login.')
+                else:
+                    try:
+                        _send_verification(self.request, user, email_template_name)
+                        message = _('Verification sent to %s' % user.email)
+                    except (SMTPException, socket.error) as e:
+                        status = messages.ERROR
+                        name = 'strerror'
+                        message = getattr(e, name) if hasattr(e, name) else e
             else:
                 _send_notification(user, admin_email_template_name)
                 message = _('Your request for an account was successfully sent \
@@ -183,7 +204,7 @@ class InvitationsBackend(object):
             transaction.commit()
         return status, message, user
 
-class SimpleBackend(object):
+class SimpleBackend(SignupBackend):
     """
     A registration backend which implements the following workflow: a user
     supplies the necessary registation information, an incative user account is
@@ -191,6 +212,7 @@ class SimpleBackend(object):
     """
     def __init__(self, request):
         self.request = request
+        super(SimpleBackend, self).__init__()
 
     def get_signup_form(self, provider):
         """
@@ -207,7 +229,14 @@ class SimpleBackend(object):
         ip = self.request.META.get('REMOTE_ADDR',
                 self.request.META.get('HTTP_X_REAL_IP', None))
         return globals()[formclass](initial_data, ip=ip)
-
+    
+    def _is_preaccepted(self, user):
+        if super(SimpleBackend, self)._is_preaccepted(user):
+            return True
+        if MODERATION_ENABLED:
+            return False
+        return True
+    
     @transaction.commit_manually
     def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
         """
@@ -233,7 +262,7 @@ class SimpleBackend(object):
         """
         user = form.save()
         status = messages.SUCCESS
-        if MODERATION_ENABLED:
+        if not self._is_preaccepted(user):
             try:
                 _send_notification(user, admin_email_template_name)
                 message = _('Your request for an account was successfully sent \
index 633312c..8ca90b1 100644 (file)
@@ -54,7 +54,7 @@ class LocalUserCreationForm(UserCreationForm):
     """
     Extends the built in UserCreationForm in several ways:
     
-    * Adds email, first_name and last_name field.
+    * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
     * The username field isn't visible and it is assigned a generated id.
     * User created is not active. 
     """
@@ -134,7 +134,9 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
         """
         super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
         self.fields.keyOrder = ['email', 'inviter', 'first_name',
-                                'last_name', 'password1', 'password2']
+                                'last_name', 'password1', 'password2',
+                                'recaptcha_challenge_field',
+                                'recaptcha_response_field']
         #set readonly form fields
         self.fields['inviter'].widget.attrs['readonly'] = True
         self.fields['email'].widget.attrs['readonly'] = True
@@ -144,7 +146,8 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
         user = super(InvitedLocalUserCreationForm, self).save(commit=False)
         level = user.invitation.inviter.level + 1
         user.level = level
-        user.invitations = INVITATIONS_PER_LEVEL[level]
+        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
+        user.email_verified = True
         if commit:
             user.save()
         return user
index a16b64d..ecc8fd3 100644 (file)
@@ -36,6 +36,7 @@ import socket
 from smtplib import SMTPException
 
 from django.core.management.base import BaseCommand, CommandError
+from django.db.utils import IntegrityError
 
 from astakos.im.functions import invite
 
@@ -63,5 +64,7 @@ class Command(BaseCommand):
                 self.stdout.write("Invitation sent to '%s'\n" % (email,))
             except (SMTPException, socket.error) as e:
                 raise CommandError("Error sending the invitation")
+            except IntegrityError, e:
+                raise CommandError("There is already an invitation for %s" % (email,))
         else:
             raise CommandError("No invitations left")
index 2a6eeb9..cf77dcd 100644 (file)
@@ -47,6 +47,10 @@ class Command(BaseCommand):
             dest='invitations',
             metavar='NUM',
             help="Update user's invitations"),
+        make_option('--level',
+            dest='level',
+            metavar='NUM',
+            help="Update user's level"),
         make_option('--password',
             dest='password',
             metavar='PASSWORD',
@@ -100,6 +104,10 @@ class Command(BaseCommand):
         if invitations is not None:
             user.invitations = int(invitations)
         
+        level = options.get('level')
+        if level is not None:
+            user.level = int(level)
+        
         password = options.get('password')
         if password is not None:
             user.set_password(password)
diff --git a/snf-astakos-app/astakos/im/migrations/0003_auto__add_unique_invitation_username.py b/snf-astakos-app/astakos/im/migrations/0003_auto__add_unique_invitation_username.py
new file mode 100644 (file)
index 0000000..3c67560
--- /dev/null
@@ -0,0 +1,87 @@
+# 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 unique constraint on 'Invitation', fields ['username']
+        db.create_unique('im_invitation', ['username'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'Invitation', fields ['username']
+        db.delete_unique('im_invitation', ['username'])
+
+
+    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.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'}),
+            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}),
+            '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.invitation': {
+            'Meta': {'object_name': 'Invitation'},
+            'accepted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            '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_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            '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']
diff --git a/snf-astakos-app/astakos/im/migrations/0004_auto__add_field_astakosuser_email_verified.py b/snf-astakos-app/astakos/im/migrations/0004_auto__add_field_astakosuser_email_verified.py
new file mode 100644 (file)
index 0000000..6b27b42
--- /dev/null
@@ -0,0 +1,88 @@
+# 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 field 'AstakosUser.email_verified'
+        db.add_column('im_astakosuser', 'email_verified', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'AstakosUser.email_verified'
+        db.delete_column('im_astakosuser', 'email_verified')
+
+
+    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.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'}),
+            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}),
+            '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.invitation': {
+            'Meta': {'object_name': 'Invitation'},
+            'accepted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            '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_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            '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']
index 9f73e0a..09c047c 100644 (file)
@@ -56,7 +56,7 @@ class AstakosUser(User):
     #for invitations
     user_level = DEFAULT_USER_LEVEL
     level = models.IntegerField('Inviter level', default=user_level)
-    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL[user_level])
+    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
     
     auth_token = models.CharField('Authentication Token', max_length=32,
                                   null=True, blank=True)
@@ -69,6 +69,8 @@ class AstakosUser(User):
     # ex. screen_name for twitter, eppn for shibboleth
     third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
     
+    email_verified = models.BooleanField('Email verified?', default=False)
+    
     @property
     def realname(self):
         return '%s %s' %(self.first_name, self.last_name)
@@ -127,7 +129,7 @@ class Invitation(models.Model):
     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
                                 null=True)
     realname = models.CharField('Real name', max_length=255)
-    username = models.CharField('Unique ID', max_length=255)
+    username = models.CharField('Unique ID', max_length=255, unique=True)
     code = models.BigIntegerField('Invitation code', db_index=True)
     #obsolete: we keep it just for transfering the data
     is_accepted = models.BooleanField('Accepted?', default=False)
index 631a5cb..4a0b1e6 100644 (file)
@@ -61,3 +61,5 @@ RECAPTCHA_OPTIONS = getattr(settings, 'ASTAKOS_RECAPTCHA_OPTIONS', {'theme': 'wh
 # Set where the user should be redirected after logout
 LOGOUT_NEXT = getattr(settings, 'ASTAKOS_LOGOUT_NEXT', '')
 
+# Set user email patterns that are automatically activated
+RE_USER_EMAIL_PATTERNS = getattr(settings, 'ASTAKOS_RE_USER_EMAIL_PATTERNS', [])
\ No newline at end of file
index 285f460..c82eebb 100644 (file)
@@ -75,7 +75,7 @@ def get_or_create_user(email, realname='', first_name='', last_name='', affiliat
             'password':password,
             'affiliation':affiliation,
             'level':level,
-            'invitations':INVITATIONS_PER_LEVEL[level],
+            'invitations':INVITATIONS_PER_LEVEL.get(level, 0),
             'provider':provider,
             'realname':realname,
             'first_name':first_name,
index 31011e6..20249a9 100644 (file)
@@ -50,6 +50,7 @@ from django.db import transaction
 from django.contrib.auth import logout as auth_logout
 from django.utils.http import urlencode
 from django.http import HttpResponseRedirect
+from django.db.utils import IntegrityError
 
 from astakos.im.models import AstakosUser, Invitation
 from astakos.im.backends import get_backend
@@ -177,6 +178,10 @@ def invite(request, template_name='im/invitations.html', extra_context={}):
                 status = messages.ERROR
                 message = getattr(e, 'strerror', '')
                 transaction.rollback()
+            except IntegrityError, e:
+                status = messages.ERROR
+                message = _('There is already invitation for %s' % username)
+                transaction.rollback()
         else:
             status = messages.ERROR
             message = _('No invitations left')
@@ -261,7 +266,7 @@ def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.
     Upon successful user creation if ``next`` url parameter is present the user is redirected there
     otherwise renders the same page with a success message.
     
-    On unsuccessful creation, renders the same page with an error message.
+    On unsuccessful creation, renders ``on_failure`` with an error message.
     
     **Arguments**
     
@@ -405,5 +410,6 @@ def activate(request):
         return HttpResponseBadRequest('No such user')
     
     user.is_active = True
+    user.email_verified = True
     user.save()
     return prepare_response(request, user, next, renew=True)