Statistics
| Branch: | Tag: | Revision:

root / astakos / im / backends.py @ e015e9e6

History | View | Annotate | Download (10 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.utils.translation import ugettext as _
39
from django.contrib.sites.models import Site
40
from django.contrib import messages
41
from django.db import transaction
42

    
43
from smtplib import SMTPException
44
from urllib import quote
45

    
46
from astakos.im.models import AstakosUser, Invitation
47
from astakos.im.forms import *
48
from astakos.im.util import get_invitation
49
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL
50

    
51
import socket
52
import logging
53

    
54
logger = logging.getLogger(__name__)
55

    
56
def get_backend(request):
57
    """
58
    Returns an instance of a registration backend,
59
    according to the INVITATIONS_ENABLED setting
60
    (if True returns ``astakos.im.backends.InvitationsBackend`` and if False
61
    returns ``astakos.im.backends.SimpleBackend``).
62
    
63
    If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
64
    is raised.
65
    """
66
    module = 'astakos.im.backends'
67
    prefix = 'Invitations' if INVITATIONS_ENABLED else 'Simple'
68
    backend_class_name = '%sBackend' %prefix
69
    try:
70
        mod = import_module(module)
71
    except ImportError, e:
72
        raise ImproperlyConfigured('Error loading registration backend %s: "%s"' % (module, e))
73
    try:
74
        backend_class = getattr(mod, backend_class_name)
75
    except AttributeError:
76
        raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
77
    return backend_class(request)
78

    
79
class InvitationsBackend(object):
80
    """
81
    A registration backend which implements the following workflow: a user
82
    supplies the necessary registation information, if the request contains a valid
83
    inivation code the user is automatically activated otherwise an inactive user
84
    account is created and the user is going to receive an email as soon as an
85
    administrator activates his/her account.
86
    """
87
    def __init__(self, request):
88
        """
89
        raises Invitation.DoesNotExist and ValueError if invitation is consumed
90
        or invitation username is reserved.
91
        """
92
        self.request = request
93
        self.invitation = get_invitation(request)
94
    
95
    def get_signup_form(self, provider):
96
        """
97
        Returns the form class name 
98
        """
99
        invitation = self.invitation
100
        initial_data = self.get_signup_initial_data(provider)
101
        prefix = 'Invited' if invitation else ''
102
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
103
        suffix  = 'UserCreationForm'
104
        formclass = '%s%s%s' % (prefix, main, suffix)
105
        return globals()[formclass](initial_data)
106
    
107
    def get_signup_initial_data(self, provider):
108
        """
109
        Returns the necassary registration form depending the user is invited or not
110
        
111
        Throws Invitation.DoesNotExist in case ``code`` is not valid.
112
        """
113
        request = self.request
114
        invitation = self.invitation
115
        initial_data = None
116
        if request.method == 'GET':
117
            if invitation:
118
                # create a tmp user with the invitation realname
119
                # to extract first and last name
120
                u = AstakosUser(realname = invitation.realname)
121
                initial_data = {'email':invitation.username,
122
                                'inviter':invitation.inviter.realname,
123
                                'first_name':u.first_name,
124
                                'last_name':u.last_name}
125
        else:
126
            if provider == request.POST.get('provider', ''):
127
                initial_data = request.POST
128
        return initial_data
129
    
130
    def _is_preaccepted(self, user):
131
        """
132
        If there is a valid, not-consumed invitation code for the specific user
133
        returns True else returns False.
134
        """
135
        invitation = self.invitation
136
        if not invitation:
137
            return False
138
        if invitation.username == user.email and not invitation.is_consumed:
139
            invitation.consume()
140
            return True
141
        return False
142
    
143
    @transaction.commit_manually
144
    def signup(self, form):
145
        """
146
        Initially creates an inactive user account. If the user is preaccepted
147
        (has a valid invitation code) the user is activated and if the request
148
        param ``next`` is present redirects to it.
149
        In any other case the method returns the action status and a message.
150
        
151
        The method uses commit_manually decorator in order to ensure the user
152
        will be created only if the procedure has been completed successfully.
153
        """
154
        user = None
155
        try:
156
            user = form.save()
157
            if self._is_preaccepted(user):
158
                user.is_active = True
159
                user.save()
160
                message = _('Registration completed. You can now login.')
161
            else:
162
                message = _('Registration completed. You will receive an email upon your account\'s activation.')
163
            status = messages.SUCCESS
164
        except Invitation.DoesNotExist, e:
165
            status = messages.ERROR
166
            message = _('Invalid invitation code')
167
        except socket.error, e:
168
            status = messages.ERROR
169
            message = _(e.strerror)
170
        
171
        # rollback in case of error
172
        if status == messages.ERROR:
173
            transaction.rollback()
174
        else:
175
            transaction.commit()
176
        return status, message, user
177

    
178
class SimpleBackend(object):
179
    """
180
    A registration backend which implements the following workflow: a user
181
    supplies the necessary registation information, an incative user account is
182
    created and receives an email in order to activate his/her account.
183
    """
184
    def __init__(self, request):
185
        self.request = request
186
    
187
    def get_signup_form(self, provider):
188
        """
189
        Returns the form class name
190
        """
191
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
192
        suffix  = 'UserCreationForm'
193
        formclass = '%s%s' % (main, suffix)
194
        request = self.request
195
        initial_data = None
196
        if request.method == 'POST':
197
            if provider == request.POST.get('provider', ''):
198
                initial_data = request.POST
199
        return globals()[formclass](initial_data)
200
    
201
    @transaction.commit_manually
202
    def signup(self, form, email_template_name='im/activation_email.txt'):
203
        """
204
        Creates an inactive user account and sends a verification email.
205
        
206
        The method uses commit_manually decorator in order to ensure the user
207
        will be created only if the procedure has been completed successfully.
208
        
209
        ** Arguments **
210
        
211
        ``email_template_name``
212
            A custom template for the verification email body to use. This is
213
            optional; if not specified, this will default to
214
            ``im/activation_email.txt``.
215
        
216
        ** Templates **
217
            im/activation_email.txt or ``email_template_name`` keyword argument
218
        
219
        ** Settings **
220
        
221
        * DEFAULT_CONTACT_EMAIL: service support email
222
        * DEFAULT_FROM_EMAIL: from email
223
        """
224
        user = None
225
        try:
226
            user = form.save()
227
            status = messages.SUCCESS
228
            _send_verification(self.request, user, email_template_name)
229
            message = _('Verification sent to %s' % user.email)
230
        except (SMTPException, socket.error) as e:
231
            status = messages.ERROR
232
            name = 'strerror'
233
            message = getattr(e, name) if hasattr(e, name) else e
234
        
235
        # rollback in case of error
236
        if status == messages.ERROR:
237
            transaction.rollback()
238
        else:
239
            transaction.commit()
240
        return status, message, user
241

    
242
def _send_verification(request, user, template_name):
243
    site = Site.objects.get_current()
244
    baseurl = request.build_absolute_uri('/').rstrip('/')
245
    url = '%s%s?auth=%s&next=%s' % (baseurl,
246
                                    reverse('astakos.im.target.activate'),
247
                                    quote(user.auth_token))
248
    message = render_to_string(template_name, {
249
            'user': user,
250
            'url': url,
251
            'baseurl': baseurl,
252
            'site_name': site.name,
253
            'support': DEFAULT_CONTACT_EMAIL % site.name.lower()})
254
    sender = DEFAULT_FROM_EMAIL % site.name
255
    send_mail('%s account activation' % site.name, message, sender, [user.email])
256
    logger.info('Sent activation %s', user)