Refine shibboleth signup mechanism
authorSofia Papagiannaki <papagian@gmail.com>
Thu, 15 Nov 2012 15:37:58 +0000 (17:37 +0200)
committerSofia Papagiannaki <papagian@gmail.com>
Thu, 15 Nov 2012 15:37:58 +0000 (17:37 +0200)
Refs: #3041

13 files changed:
snf-astakos-app/astakos/im/auth_backends.py
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/functions.py
snf-astakos-app/astakos/im/migrations/0015_auto__add_pendingthirdpartyuser.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/target/local.py
snf-astakos-app/astakos/im/target/shibboleth.py
snf-astakos-app/astakos/im/templates/im/login_base.html
snf-astakos-app/astakos/im/templates/im/third_party_check_local.html [new file with mode: 0644]
snf-astakos-app/astakos/im/templates/im/third_party_registration.html
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/astakos/im/views.py

index 0d4c830..588cca3 100644 (file)
@@ -64,10 +64,12 @@ class EmailBackend(ModelBackend):
     def authenticate(self, username=None, password=None):
         #If username is an email address, then try to pull it up
         if email_re.search(username):
-            try:
-                user = AstakosUser.objects.get(email=username, is_active=True)
-            except AstakosUser.DoesNotExist:
+            users = AstakosUser.objects.filter(email=username)
+            if not users:
                 return None
+            for user in users:
+                if  user.check_password(password):
+                    return user
         else:
             #We have a non-email address username we
             #should try username
index a73926e..28c76a5 100644 (file)
@@ -48,10 +48,11 @@ from django.contrib import messages
 from django.utils.encoding import smart_str
 
 from astakos.im.models import AstakosUser, Invitation, get_latest_terms, EmailChange
-from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, \
-    BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, \
-    RECAPTCHA_ENABLED, LOGGING_LEVEL, PASSWORD_RESET_EMAIL_SUBJECT, \
-    NEWPASSWD_INVALIDATE_TOKEN
+from astakos.im.settings import (INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL,
+    BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL,
+    RECAPTCHA_ENABLED, LOGGING_LEVEL, PASSWORD_RESET_EMAIL_SUBJECT,
+    NEWPASSWD_INVALIDATE_TOKEN, THIRDPARTY_ACC_ADDITIONAL_FIELDS
+)
 from astakos.im.widgets import DummyWidget, RecaptchaWidget
 from astakos.im.functions import send_change_email
 
@@ -182,6 +183,10 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
         return user
 
 class ThirdPartyUserCreationForm(forms.ModelForm):
+    third_party_identifier = forms.CharField(
+        widget=forms.HiddenInput(),
+        label=''
+    )
     class Meta:
         model = AstakosUser
         fields = ("email", "first_name", "last_name", "third_party_identifier", "has_signed_terms")
@@ -193,15 +198,16 @@ class ThirdPartyUserCreationForm(forms.ModelForm):
         self.request = kwargs.get('request', None)
         if self.request:
             kwargs.pop('request')
+                
+        latest_terms = get_latest_terms()
+        if latest_terms:
+            self._meta.fields.append('has_signed_terms')
+                
         super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
-        self.fields.keyOrder = ['email', 'first_name', 'last_name', 'third_party_identifier']
-        if get_latest_terms():
+        
+        if latest_terms:
             self.fields.keyOrder.append('has_signed_terms')
-        #set readonly form fields
-        ro = ["third_party_identifier"]
-        for f in ro:
-            self.fields[f].widget.attrs['readonly'] = True
-
+        
         if 'has_signed_terms' in self.fields:
             # Overriding field label since we need to apply a link
             # to the terms within the label
@@ -262,20 +268,23 @@ class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
 
     def __init__(self, *args, **kwargs):
         super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
-        self.fields.keyOrder.append('additional_email')
         # copy email value to additional_mail in case user will change it
         name = 'email'
         field = self.fields[name]
         self.initial['additional_email'] = self.initial.get(name, field.initial)
-
+        self.initial['email'] = None
+    
     def clean_email(self):
         email = self.cleaned_data['email']
         for user in AstakosUser.objects.filter(email = email):
             if user.provider == 'shibboleth':
-                raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
-            elif not user.is_active:
-                raise forms.ValidationError(_("This email is already associated with an inactive account. \
-                                              You need to wait to be activated before being able to switch to a shibboleth account."))
+                raise forms.ValidationError(_(
+                        "This email is already associated with another shibboleth \
+                        account."
+                    )
+                )
+            else:
+                raise forms.ValidationError(_("This email is already used"))
         super(ShibbolethUserCreationForm, self).clean_email()
         return email
 
@@ -321,7 +330,7 @@ class LoginForm(AuthenticationForm):
         check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
         if not check.is_valid:
             raise forms.ValidationError(_('You have not entered the correct words'))
-
+    
     def clean(self):
         super(LoginForm, self).clean()
         if self.user_cache and self.user_cache.provider not in ('local', ''):
index f3fd438..4e2db2d 100644 (file)
@@ -249,23 +249,6 @@ def activate(user, email_template_name='im/welcome_email.txt',
     send_helpdesk_notification(user, helpdesk_email_template_name)
     send_greeting(user, email_template_name)
 
-def switch_account_to_shibboleth(user, local_user, greeting_template_name='im/welcome_email.txt'):
-    if not user or not isinstance(user, AstakosUser):
-        return
-    
-    if not local_user or not isinstance(user, AstakosUser):
-        return
-    
-    if not user.provider == 'shibboleth':
-        return
-    
-    user.delete()
-    local_user.provider = 'shibboleth'
-    local_user.third_party_identifier = user.third_party_identifier
-    local_user.save()
-    send_greeting(local_user, greeting_template_name)
-    return local_user
-
 def invite(invitation, inviter, email_template_name='im/welcome_email.txt'):
     """
     Send an invitation email and upon success reduces inviter's invitation by one.
diff --git a/snf-astakos-app/astakos/im/migrations/0015_auto__add_pendingthirdpartyuser.py b/snf-astakos-app/astakos/im/migrations/0015_auto__add_pendingthirdpartyuser.py
new file mode 100644 (file)
index 0000000..b98787c
--- /dev/null
@@ -0,0 +1,141 @@
+# 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 model 'PendingThirdPartyUser'
+        db.create_table('im_pendingthirdpartyuser', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('third_party_identifier', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+            ('provider', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
+            ('email', self.gf('django.db.models.fields.EmailField')(max_length=75, blank=True)),
+            ('first_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)),
+            ('last_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)),
+            ('affiliation', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
+            ('username', self.gf('django.db.models.fields.CharField')(unique=True, max_length=30)),
+        ))
+        db.send_create_signal('im', ['PendingThirdPartyUser'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'PendingThirdPartyUser'
+        db.delete_table('im_pendingthirdpartyuser')
+
+
+    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.additionalmail': {
+            'Meta': {'object_name': 'AdditionalMail'},
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"})
+        },
+        'im.approvalterms': {
+            'Meta': {'object_name': 'ApprovalTerms'},
+            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 15, 13, 7, 38, 626139)', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'location': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'im.astakosuser': {
+            'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
+            'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            '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'}),
+            'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '100'}),
+            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            '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.emailchange': {
+            'Meta': {'object_name': 'EmailChange'},
+            'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+            'requested_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 15, 13, 7, 38, 627944)'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchange_user'", 'unique': 'True', 'to': "orm['im.AstakosUser']"})
+        },
+        'im.invitation': {
+            'Meta': {'object_name': 'Invitation'},
+            '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_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'})
+        },
+        'im.pendingthirdpartyuser': {
+            'Meta': {'object_name': 'PendingThirdPartyUser'},
+            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            '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'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'im.service': {
+            'Meta': {'object_name': 'Service'},
+            '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'}),
+            'icon': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'url': ('django.db.models.fields.FilePathField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['im']
index b2564af..3600810 100644 (file)
@@ -39,7 +39,7 @@ import json
 from time import asctime
 from datetime import datetime, timedelta
 from base64 import b64encode
-from urlparse import urlparse, urlunparse
+from urlparse import urlparse
 from random import randint
 
 from django.db import models, IntegrityError
@@ -374,6 +374,42 @@ class AdditionalMail(models.Model):
     owner = models.ForeignKey(AstakosUser)
     email = models.EmailField()
 
+class PendingThirdPartyUser(models.Model):
+    """
+    Model for registring successful third party user authentications
+    """
+    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
+    provider = models.CharField('Provider', max_length=255, blank=True)
+    email = models.EmailField(_('e-mail address'), blank=True)
+    first_name = models.CharField(_('first name'), max_length=30, blank=True)
+    last_name = models.CharField(_('last name'), max_length=30, blank=True)
+    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
+    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
+
+    @property
+    def realname(self):
+        return '%s %s' %(self.first_name, self.last_name)
+
+    @realname.setter
+    def realname(self, value):
+        parts = value.split(' ')
+        if len(parts) == 2:
+            self.first_name = parts[0]
+            self.last_name = parts[1]
+        else:
+            self.last_name = parts[0]
+    
+    def save(self, **kwargs):
+        if not self.id:
+            # set username
+            while not self.username:
+                username =  uuid.uuid4().hex[:30]
+                try:
+                    AstakosUser.objects.get(username = username)
+                except AstakosUser.DoesNotExist, e:
+                    self.username = username
+        super(PendingThirdPartyUser, self).save(**kwargs)
+
 def create_astakos_user(u):
     try:
         AstakosUser.objects.get(user_ptr=u.pk)
index da60994..e674108 100644 (file)
@@ -119,4 +119,14 @@ PASSWORD_RESET_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_PASSWORD_RESET_EMAIL_S
         'Password reset on %s alpha2 testing' % SITENAME)
 
 # Enforce token renewal on password change/reset
-NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
\ No newline at end of file
+NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
+
+# Permit local account migration
+ENABLE_LOCAL_ACCOUNT_MIGRATION = getattr(settings, 'ASTAKOS_ENABLE_LOCAL_ACCOUNT_MIGRATION', True)
+
+# A dictionary describing the user fields appearing during the second step of third party account creation
+from django import forms
+THIRDPARTY_ACC_ADDITIONAL_FIELDS = getattr(settings, 'ASTAKOS_THIRDPARTY_ACC_ADDITIONAL_FIELDS', {
+    'first_name':None,
+    'last_name':None,
+})
\ No newline at end of file
index 25c02cd..d993fdb 100644 (file)
@@ -39,12 +39,14 @@ from django.contrib import messages
 from django.utils.translation import ugettext as _
 from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.http import require_http_methods
+from django.core.urlresolvers import reverse
 
 from astakos.im.util import prepare_response, get_query
 from astakos.im.views import requires_anonymous
-from astakos.im.models import AstakosUser
+from astakos.im.models import AstakosUser, PendingThirdPartyUser
 from astakos.im.forms import LoginForm
 from astakos.im.settings import RATELIMIT_RETRIES_ALLOWED
+from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION
 
 from ratelimit.decorators import ratelimit
 
@@ -62,11 +64,16 @@ def login(request, on_failure='im/login.html'):
     was_limited = getattr(request, 'limited', False)
     form = LoginForm(data=request.POST, was_limited=was_limited, request=request)
     next = get_query(request).get('next', '')
+    username = get_query(request).get('key')
+    
     if not form.is_valid():
-        return render_to_response(on_failure,
-                                  {'login_form':form,
-                                   'next':next},
-                                  context_instance=RequestContext(request))
+        return render_to_response(
+            on_failure,
+            {'login_form':form,
+             'next':next,
+             'key':username},
+            context_instance=RequestContext(request)
+        )
     # get the user from the cash
     user = form.user_cache
     
@@ -74,11 +81,39 @@ def login(request, on_failure='im/login.html'):
     if not user:
         message = _('Cannot authenticate account')
     elif not user.is_active:
-        message = _('Inactive account')
+        if user.sent_activation:
+            message = _('Your request is pending activation')
+        else:
+            message = _('You have not followed the activation link')
     if message:
         messages.add_message(request, messages.ERROR, message)
         return render_to_response(on_failure,
                                   {'form':form},
                                   context_instance=RequestContext(request))
     
-    return prepare_response(request, user, next)
+    # hook for switching account to use third party authentication
+    if ENABLE_LOCAL_ACCOUNT_MIGRATION and username:
+        try:
+            new = PendingThirdPartyUser.objects.get(
+                username=username)
+        except:
+            messages.error(
+                request,
+                _('Account failed to switch to %(provider)s' % locals())
+            )
+            return render_to_response(
+                on_failure,
+                {'login_form':form,
+                 'next':next},
+                context_instance=RequestContext(request)
+            )
+        else:
+            user.provider = new.provider
+            user.third_party_identifier = new.third_party_identifier
+            user.save()
+            new.delete()
+            messages.success(
+                request,
+                _('Account successfully switched to %(provider)s' % user.__dict__)
+            )
+    return prepare_response(request, user, next)
\ No newline at end of file
index f54b2de..955c7e3 100644 (file)
@@ -36,14 +36,25 @@ from django.utils.translation import ugettext as _
 from django.contrib import messages
 from django.template import RequestContext
 from django.views.decorators.http import require_http_methods
+from django.db.models import Q
+from django.core.exceptions import ValidationError
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
+from urlparse import urlunsplit, urlsplit
+from django.utils.http import urlencode
 
 from astakos.im.util import prepare_response, get_context, get_invitation
 from astakos.im.views import requires_anonymous, render_response
-from astakos.im.settings import DEFAULT_USER_LEVEL
-from astakos.im.models import AstakosUser, Invitation, AdditionalMail
+from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
+
+from astakos.im.models import AstakosUser, PendingThirdPartyUser
 from astakos.im.forms import LoginForm
 from astakos.im.activation_backends import get_backend, SimpleBackend
 
+import logging
+
+logger = logging.getLogger(__name__)
+
 class Tokens:
     # these are mapped by the Shibboleth SP software
     SHIB_EPPN = "HTTP_EPPN" # eduPersonPrincipalName
@@ -57,7 +68,13 @@ class Tokens:
 
 @require_http_methods(["GET", "POST"])
 @requires_anonymous
-def login(request,  backend=None, on_login_template='im/login.html', on_creation_template='im/third_party_registration.html', extra_context={}):
+def login(
+    request,
+    on_login_template='im/login.html',
+    on_signup_template='im/third_party_check_local.html',
+    extra_context=None):
+    extra_context = extra_context or {}
+
     tokens = request.META
     
     try:
@@ -75,10 +92,13 @@ def login(request,  backend=None, on_login_template='im/login.html', on_creation
         return HttpResponseBadRequest("Missing user name in request")
     
     affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
-    email = tokens.get(Tokens.SHIB_MAIL, None)
-    
+    email = tokens.get(Tokens.SHIB_MAIL, '')
+        
     try:
-        user = AstakosUser.objects.get(provider='shibboleth', third_party_identifier=eppn)
+        user = AstakosUser.objects.get(
+            provider='shibboleth',
+            third_party_identifier=eppn
+        )
         if user.is_active:
             return prepare_response(request,
                                     user,
@@ -91,17 +111,74 @@ def login(request,  backend=None, on_login_template='im/login.html', on_creation
                                    login_form = LoginForm(request=request),
                                    context_instance=RequestContext(request))
     except AstakosUser.DoesNotExist, e:
-        user = AstakosUser(third_party_identifier=eppn, realname=realname,
-                           affiliation=affiliation, provider='shibboleth',
-                           email=email)
+        # First time
+        try:
+            user, created = PendingThirdPartyUser.objects.get_or_create(
+                third_party_identifier=eppn,
+                provider='shibboleth',
+                defaults=dict(
+                    realname=realname,
+                    affiliation=affiliation,
+                    email=email
+                )
+            )
+            user.save()
+        except BaseException, e:
+            logger.exception(e)
+            template = on_login_template
+            extra_context['login_form'] = LoginForm(request=request)
+            messages.error(request, _('Something went wrong.'))
+        else:
+            if not ENABLE_LOCAL_ACCOUNT_MIGRATION:
+                url = reverse(
+                    'astakos.im.target.shibboleth.signup'
+                )
+                parts = list(urlsplit(url))
+                parts[3] = urlencode({'key': user.username})
+                url = urlunsplit(parts)
+                return HttpResponseRedirect(url)
+            else:
+                template = on_signup_template
+                extra_context['key'] = user.username
+        
+        extra_context['provider']='shibboleth'
+        return render_response(
+            template,
+            context_instance=get_context(request, extra_context)
+        )
+
+@require_http_methods(["GET"])
+@requires_anonymous
+def signup(request,
+           backend=None,
+           on_creation_template='im/third_party_registration.html',
+           extra_context=None
+):
+    extra_context = extra_context or {}
+    username = request.GET.get('key')
+    if not username:
+        return HttpResponseBadRequest(_('Missing key parameter.'))
+    try:
+        pending = PendingThirdPartyUser.objects.get(username=username)
+    except BaseException, e:
+        logger.exception(e)
+        return HttpResponseBadRequest(_('Invalid key.'))
+    else:
+        d = pending.__dict__
+        d.pop('_state', None)
+        d.pop('id', None)
+        user = AstakosUser(**d)
         try:
-            if not backend:
-                backend = get_backend(request)
-            form = backend.get_signup_form(provider='shibboleth', instance=user)
-        except Exception, e:
-            form = SimpleBackend(request).get_signup_form(provider='shibboleth', instance=user)
-            messages.add_message(request, messages.ERROR, e)
-        return render_response(on_creation_template,
-                               signup_form = form,
-                               provider = 'shibboleth',
-                               context_instance=get_context(request, extra_context))
+            backend = backend or get_backend(request)
+        except ImproperlyConfigured, e:
+            messages.error(request, e)
+        else:
+            extra_context['form'] = backend.get_signup_form(
+                provider='shibboleth',
+                instance=user
+            )
+    extra_context['provider']='shibboleth'
+    return render_response(
+            on_creation_template,
+            context_instance=get_context(request, extra_context)
+    )
\ No newline at end of file
index 33b1872..0d9c56f 100644 (file)
                        {% include "im/form_render.html" %}
                    {% endwith %}
                    <input type="hidden" name="next" value="{{ next }}">
+                   
+                   {% if key %}
+                       <input type="hidden" name="key" value="{{key}}">
+                   {% else %}
+                       {% if request.GET.key %}
+                           <input type="hidden" name="key" value="{{request.GET.key}}">
+                       {% endif %}
+                   {% endif %}
+                   
                    <div class="form-row submit clearfix">
-                       
-                       <input type="submit" class="submit altcol" value="SUBMIT" />
+                       <input type="submit" class="submit altcol" value="SUBMIT" />
                        <a class="extra-link" href="{% url django.contrib.auth.views.password_reset %}">Forgot your password?</a>
                    </div>
        </form>
diff --git a/snf-astakos-app/astakos/im/templates/im/third_party_check_local.html b/snf-astakos-app/astakos/im/templates/im/third_party_check_local.html
new file mode 100644 (file)
index 0000000..1d29e13
--- /dev/null
@@ -0,0 +1,20 @@
+{% extends 'im/base_two_cols.html' %}
+
+{% block page.title %}
+    Signup
+{% endblock %}
+
+{% block body.left %}
+    <img class="pic" src="{{ IM_STATIC_URL }}images/pictures/accounts_3.png" />
+{% comment %}{% include "im/services_description.html" %}{% endcomment %}
+{% endblock body.left %}
+
+{% block body.right %}
+    {% if "local" in im_modules %}
+      <div class="form-stacked">
+        <h2><span>Already have an account?</span></h2>
+        <a href="{% url astakos.im.views.index %}?key={{key}}">YES</a>
+        <a href="{% url astakos.im.target.shibboleth.signup %}?key={{key}}">NO</a>
+      </div>
+    {% endif %}
+{% endblock %}
\ No newline at end of file
index 8cc7a0a..4e2c85d 100644 (file)
       <div class="form-stacked">
         <form action="{% url astakos.im.views.signup %}" method="post"
             class="innerlabels signup">{% csrf_token %}
-          <h2><span>Provide an email address to complete the registration:</span></h2>
+          <h2><span>SIGN UP</span></h2>
             <input type="hidden" name="next" value="{{ next }}">
             <input type="hidden" name="code" value="{{ code }}">
             <input type="hidden" name="provider" value={{ provider|default:"local" }}>
-            {% with signup_form as form %}
             {% include "im/form_render.html" %}
-            {% endwith %}
             <div class="form-row submit">
                 <input type="submit" class="submit altcol" value="SUBMIT" />
             </div>
index 0bb3edf..44d5fb9 100644 (file)
@@ -86,7 +86,8 @@ if INVITATIONS_ENABLED:
 
 if 'shibboleth' in IM_MODULES:
     urlpatterns += patterns('astakos.im.target',
-        url(r'^login/shibboleth/?$', 'shibboleth.login')
+        url(r'^login/shibboleth/?$', 'shibboleth.login'),
+        url(r'^login/shibboleth/signup/?$', 'shibboleth.signup')
     )
 
 if 'twitter' in IM_MODULES:
index f12e356..684be53 100644 (file)
@@ -52,16 +52,18 @@ from django.http import HttpResponseRedirect, HttpResponseBadRequest
 from django.db.utils import IntegrityError
 from django.contrib.auth.views import password_change
 from django.core.exceptions import ValidationError
-from django.db.models import Q
 from django.views.decorators.http import require_http_methods
 
 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
 from astakos.im.activation_backends import get_backend, SimpleBackend
 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
 from astakos.im.forms import *
-from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
-    invite as invite_func, logout as auth_logout, activate as activate_func, switch_account_to_shibboleth
-from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
+from astakos.im.functions import (send_greeting, send_feedback, SendMailError,
+    invite as invite_func, logout as auth_logout, activate as activate_func
+)
+from astakos.im.settings import (DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL,
+    COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
+)
 
 logger = logging.getLogger(__name__)
 
@@ -137,9 +139,12 @@ def index(request, login_template_name='im/login.html', profile_template_name='i
     template_name = login_template_name
     if request.user.is_authenticated():
         return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
-    return render_response(template_name,
-                           login_form = LoginForm(request=request),
-                           context_instance = get_context(request, extra_context))
+    
+    return render_response(
+        template_name,
+        login_form = LoginForm(request=request),
+        context_instance = get_context(request, extra_context)
+    )
 
 @require_http_methods(["GET", "POST"])
 @login_required
@@ -462,43 +467,22 @@ def activate(request, greeting_email_template_name='im/welcome_email.txt', helpd
         return index(request)
     
     try:
-        local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True)
-    except AstakosUser.DoesNotExist:
-        try:
-            activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
-            response = prepare_response(request, user, next, renew=True)
-            transaction.commit()
-            return response
-        except SendMailError, e:
-            message = e.message
-            messages.add_message(request, messages.ERROR, message)
-            transaction.rollback()
-            return index(request)
-        except BaseException, e:
-            status = messages.ERROR
-            message = _('Something went wrong.')
-            messages.add_message(request, messages.ERROR, message)
-            logger.exception(e)
-            transaction.rollback()
-            return index(request)
-    else:
-        try:
-            user = switch_account_to_shibboleth(user, local_user, greeting_email_template_name)
-            response = prepare_response(request, user, next, renew=True)
-            transaction.commit()
-            return response
-        except SendMailError, e:
-            message = e.message
-            messages.add_message(request, messages.ERROR, message)
-            transaction.rollback()
-            return index(request)
-        except BaseException, e:
-            status = messages.ERROR
-            message = _('Something went wrong.')
-            messages.add_message(request, messages.ERROR, message)
-            logger.exception(e)
-            transaction.rollback()
-            return index(request)
+        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
+        response = prepare_response(request, user, next, renew=True)
+        transaction.commit()
+        return response
+    except SendMailError, e:
+        message = e.message
+        messages.add_message(request, messages.ERROR, message)
+        transaction.rollback()
+        return index(request)
+    except BaseException, e:
+        status = messages.ERROR
+        message = _('Something went wrong.')
+        messages.add_message(request, messages.ERROR, message)
+        logger.exception(e)
+        transaction.rollback()
+        return index(request)
 
 @require_http_methods(["GET", "POST"])
 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):