Commit 8316698a authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

Automatically activate users whose email matches specific email patterns...

Automatically activate users whose email matches specific email patterns defined in settings (ASTAKOS_RE_USER_EMAIL_PATTERNS) &  minor other fixes:

* 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
parent 3a76cacb
......@@ -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
......
......@@ -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 \
......
......@@ -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
......
......@@ -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")
......@@ -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)
......
# 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']
# 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']
......@@ -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)
......
......@@ -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
......@@ -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,
......
......@@ -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)
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