Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / activation_backends.py @ 12d07b02

History | View | Annotate | Download (10.4 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from django.utils.importlib import import_module
35
from django.core.exceptions import ImproperlyConfigured
36
from django.core.mail import send_mail
37
from django.template.loader import render_to_string
38
from django.contrib import messages
39
from django.core.urlresolvers import reverse
40
from django.utils.translation import ugettext as _
41
from django.db import transaction
42

    
43
from urlparse import urljoin
44

    
45
from astakos.im.models import AstakosUser, Invitation
46
from astakos.im.forms import *
47
from astakos.im.util import get_invitation
48
from astakos.im.functions import send_verification, send_admin_notification, activate, SendMailError
49
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
50

    
51
import socket
52
import logging
53
import re
54

    
55
logger = logging.getLogger(__name__)
56

    
57
def get_backend(request):
58
    """
59
    Returns an instance of an activation backend,
60
    according to the INVITATIONS_ENABLED setting
61
    (if True returns ``astakos.im.activation_backends.InvitationsBackend`` and if False
62
    returns ``astakos.im.activation_backends.SimpleBackend``).
63

64
    If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
65
    is raised.
66
    """
67
    module = 'astakos.im.activation_backends'
68
    prefix = 'Invitations' if INVITATIONS_ENABLED else 'Simple'
69
    backend_class_name = '%sBackend' %prefix
70
    try:
71
        mod = import_module(module)
72
    except ImportError, e:
73
        raise ImproperlyConfigured('Error loading activation backend %s: "%s"' % (module, e))
74
    try:
75
        backend_class = getattr(mod, backend_class_name)
76
    except AttributeError:
77
        raise ImproperlyConfigured('Module "%s" does not define a activation backend named "%s"' % (module, attr))
78
    return backend_class(request)
79

    
80
class ActivationBackend(object):
81
    def _is_preaccepted(self, user):
82
        # return True if user email matches specific patterns
83
        for pattern in RE_USER_EMAIL_PATTERNS:
84
            if re.match(pattern, user.email):
85
                return True
86
        return False
87
    
88
    def get_signup_form(self, provider='local', instance=None):
89
        """
90
        Returns the form class name
91
        """
92
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
93
        suffix  = 'UserCreationForm'
94
        formclass = '%s%s' % (main, suffix)
95
        request = self.request
96
        initial_data = None
97
        if request.method == 'POST':
98
            if provider == request.POST.get('provider', ''):
99
                initial_data = request.POST
100
        return globals()[formclass](initial_data, instance=instance, request=request)
101
    
102
    def handle_activation(self, user, \
103
                          verification_template_name='im/activation_email.txt', \
104
                          greeting_template_name='im/welcome_email.txt', \
105
                          admin_email_template_name='im/admin_notification.txt', \
106
                          switch_accounts_email_template_name='im/switch_accounts_email.txt'):
107
        """
108
        If the user is already active returns immediately.
109
        If the user is not active and there is another account associated with
110
        the specific email, it sends an informative email to the user whether
111
        wants to switch to this account.
112
        If the user is preaccepted and the email is verified, the account is
113
        activated automatically. Otherwise, if the email is not verified,
114
        it sends a verification email to the user.
115
        If the user is not preaccepted, it sends an email to the administrators
116
        and informs the user that the account is pending activation.
117
        """
118
        try:
119
            if user.is_active:
120
                return RegistationCompleted()
121
            if user.conflicting_email():
122
                send_verification(user, switch_accounts_email_template_name)
123
                return SwitchAccountsVerificationSent(user.email)
124
            
125
            if self._is_preaccepted(user):
126
                if user.email_verified:
127
                    activate(user, greeting_template_name)
128
                    return RegistationCompleted()
129
                else:
130
                    send_verification(user, verification_template_name)
131
                    return VerificationSent()
132
            else:
133
                send_admin_notification(user, admin_email_template_name)
134
                return NotificationSent()
135
        except BaseException, e:
136
            logger.exception(e)
137
            raise e
138

    
139
class InvitationsBackend(ActivationBackend):
140
    """
141
    A activation backend which implements the following workflow: a user
142
    supplies the necessary registation information, if the request contains a valid
143
    inivation code the user is automatically activated otherwise an inactive user
144
    account is created and the user is going to receive an email as soon as an
145
    administrator activates his/her account.
146
    """
147
    def __init__(self, request):
148
        self.request = request
149
        super(InvitationsBackend, self).__init__()
150

    
151
    def get_signup_form(self, provider='local', instance=None):
152
        """
153
        Returns the form class
154
        
155
        raises Invitation.DoesNotExist and ValueError if invitation is consumed
156
        or invitation username is reserved.
157
        """
158
        self.invitation = get_invitation(self.request)
159
        invitation = self.invitation
160
        initial_data = self.get_signup_initial_data(provider)
161
        prefix = 'Invited' if invitation else ''
162
        main = provider.capitalize()
163
        suffix  = 'UserCreationForm'
164
        formclass = '%s%s%s' % (prefix, main, suffix)
165
        return globals()[formclass](initial_data, instance=instance, request=self.request)
166

    
167
    def get_signup_initial_data(self, provider):
168
        """
169
        Returns the necassary activation form depending the user is invited or not
170

171
        Throws Invitation.DoesNotExist in case ``code`` is not valid.
172
        """
173
        request = self.request
174
        invitation = self.invitation
175
        initial_data = None
176
        if request.method == 'GET':
177
            if invitation:
178
                # create a tmp user with the invitation realname
179
                # to extract first and last name
180
                u = AstakosUser(realname = invitation.realname)
181
                initial_data = {'email':invitation.username,
182
                                'inviter':invitation.inviter.realname,
183
                                'first_name':u.first_name,
184
                                'last_name':u.last_name,
185
                                'provider':provider}
186
        else:
187
            if provider == request.POST.get('provider', ''):
188
                initial_data = request.POST
189
        return initial_data
190

    
191
    def _is_preaccepted(self, user):
192
        """
193
        If there is a valid, not-consumed invitation code for the specific user
194
        returns True else returns False.
195
        """
196
        if super(InvitationsBackend, self)._is_preaccepted(user):
197
            return True
198
        invitation = self.invitation
199
        if not invitation:
200
            return False
201
        if invitation.username == user.email and not invitation.is_consumed:
202
            invitation.consume()
203
            return True
204
        return False
205

    
206
class SimpleBackend(ActivationBackend):
207
    """
208
    A activation backend which implements the following workflow: a user
209
    supplies the necessary registation information, an incative user account is
210
    created and receives an email in order to activate his/her account.
211
    """
212
    def __init__(self, request):
213
        self.request = request
214
        super(SimpleBackend, self).__init__()
215
    
216
    def _is_preaccepted(self, user):
217
        if super(SimpleBackend, self)._is_preaccepted(user):
218
            return True
219
        if MODERATION_ENABLED:
220
            return False
221
        return True
222

    
223
class ActivationResult(object):
224
    def __init__(self, message):
225
        self.message = message
226

    
227
class VerificationSent(ActivationResult):
228
    def __init__(self):
229
        message = _('Verification sent.')
230
        super(VerificationSent, self).__init__(message)
231

    
232
class SwitchAccountsVerificationSent(ActivationResult):
233
    def __init__(self, email):
234
        message = _('This email is already associated with another \
235
                    local account. To change this account to a shibboleth \
236
                    one follow the link in the verification email sent \
237
                    to %s. Otherwise just ignore it.' % email)
238
        super(SwitchAccountsVerificationSent, self).__init__(message)
239

    
240
class NotificationSent(ActivationResult):
241
    def __init__(self):
242
        message = _('Your request for an account was successfully received and is now pending \
243
                    approval. You will be notified by email in the next few days. Thanks for \
244
                    your interest in ~okeanos! The GRNET team.')
245
        super(NotificationSent, self).__init__(message)
246

    
247
class RegistationCompleted(ActivationResult):
248
    def __init__(self):
249
        message = _('Registration completed. You can now login.')
250
        super(RegistationCompleted, self).__init__(message)