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

from django.conf import settings
from django.utils.importlib import import_module
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
36
37
38
39
from django.core.exceptions import ImproperlyConfigured
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
root's avatar
root committed
40
from django.contrib.sites.models import Site
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
41
from django.contrib import messages
42
from django.db import transaction
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
43
44
45
46
47

from smtplib import SMTPException
from urllib import quote

from astakos.im.models import AstakosUser, Invitation
48
49
from astakos.im.forms import *
from astakos.im.util import get_invitation
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
50

51
52
53
54
import socket
import logging

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

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

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

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