Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / backends.py @ db7fecd9

History | View | Annotate | Download (10.2 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
from django.core.urlresolvers import reverse
43

    
44
from smtplib import SMTPException
45
from urllib import quote
46
from urlparse import urljoin
47

    
48
from astakos.im.models import AstakosUser, Invitation
49
from astakos.im.forms import *
50
from astakos.im.util import get_invitation
51
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL
52

    
53
import socket
54
import logging
55

    
56
logger = logging.getLogger(__name__)
57

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

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

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

    
246
def _send_verification(request, user, template_name):
247
    url = '%s?auth=%s&next=%s' % (urljoin(BASEURL, reverse('astakos.im.views.activate')),
248
                                    quote(user.auth_token),
249
                                    quote(BASEURL))
250
    message = render_to_string(template_name, {
251
            'user': user,
252
            'url': url,
253
            'baseurl': BASEURL,
254
            'site_name': SITENAME,
255
            'support': DEFAULT_CONTACT_EMAIL})
256
    sender = DEFAULT_FROM_EMAIL
257
    send_mail('%s account activation' % SITENAME, message, sender, [user.email])
258
    logger.info('Sent activation %s', user)