* 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
(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
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__)
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
"""
self.request = request
self.invitation = get_invitation(request)
+ super(InvitationsBackend, self).__init__()
def get_signup_form(self, provider):
"""
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
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
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 \
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
"""
def __init__(self, request):
self.request = request
+ super(SimpleBackend, self).__init__()
def get_signup_form(self, provider):
"""
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'):
"""
"""
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 \
"""
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.
"""
"""
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
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
from smtplib import SMTPException
from django.core.management.base import BaseCommand, CommandError
+from django.db.utils import IntegrityError
from astakos.im.functions import invite
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")
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',
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)
--- /dev/null
+# 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']
--- /dev/null
+# 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']
#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)
# 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)
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)
# 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
'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,
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
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')
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**
return HttpResponseBadRequest('No such user')
user.is_active = True
+ user.email_verified = True
user.save()
return prepare_response(request, user, next, renew=True)