Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / backends.py @ 8316698a

History | View | Annotate | Download (12.8 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, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
52

    
53
import socket
54
import logging
55
import re
56

    
57
logger = logging.getLogger(__name__)
58

    
59
def get_backend(request):
60
    """
61
    Returns an instance of a registration backend,
62
    according to the INVITATIONS_ENABLED setting
63
    (if True returns ``astakos.im.backends.InvitationsBackend`` and if False
64
    returns ``astakos.im.backends.SimpleBackend``).
65

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

    
82
class SignupBackend(object):
83
    def _is_preaccepted(self, user):
84
        # return True if user email matches specific patterns
85
        for pattern in RE_USER_EMAIL_PATTERNS:
86
            if re.match(pattern, user.email):
87
                return True
88
        return False
89

    
90
class InvitationsBackend(SignupBackend):
91
    """
92
    A registration backend which implements the following workflow: a user
93
    supplies the necessary registation information, if the request contains a valid
94
    inivation code the user is automatically activated otherwise an inactive user
95
    account is created and the user is going to receive an email as soon as an
96
    administrator activates his/her account.
97
    """
98
    def __init__(self, request):
99
        """
100
        raises Invitation.DoesNotExist and ValueError if invitation is consumed
101
        or invitation username is reserved.
102
        """
103
        self.request = request
104
        self.invitation = get_invitation(request)
105
        super(InvitationsBackend, self).__init__()
106

    
107
    def get_signup_form(self, provider):
108
        """
109
        Returns the form class name
110
        """
111
        invitation = self.invitation
112
        initial_data = self.get_signup_initial_data(provider)
113
        prefix = 'Invited' if invitation else ''
114
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
115
        suffix  = 'UserCreationForm'
116
        formclass = '%s%s%s' % (prefix, main, suffix)
117
        ip = self.request.META.get('REMOTE_ADDR',
118
                self.request.META.get('HTTP_X_REAL_IP', None))
119
        return globals()[formclass](initial_data, ip=ip)
120

    
121
    def get_signup_initial_data(self, provider):
122
        """
123
        Returns the necassary registration form depending the user is invited or not
124

125
        Throws Invitation.DoesNotExist in case ``code`` is not valid.
126
        """
127
        request = self.request
128
        invitation = self.invitation
129
        initial_data = None
130
        if request.method == 'GET':
131
            if invitation:
132
                # create a tmp user with the invitation realname
133
                # to extract first and last name
134
                u = AstakosUser(realname = invitation.realname)
135
                initial_data = {'email':invitation.username,
136
                                'inviter':invitation.inviter.realname,
137
                                'first_name':u.first_name,
138
                                'last_name':u.last_name}
139
        else:
140
            if provider == request.POST.get('provider', ''):
141
                initial_data = request.POST
142
        return initial_data
143

    
144
    def _is_preaccepted(self, user):
145
        """
146
        If there is a valid, not-consumed invitation code for the specific user
147
        returns True else returns False.
148
        """
149
        if super(InvitationsBackend, self)._is_preaccepted(user):
150
            return True
151
        invitation = self.invitation
152
        if not invitation:
153
            return False
154
        if invitation.username == user.email and not invitation.is_consumed:
155
            invitation.consume()
156
            return True
157
        return False
158

    
159
    @transaction.commit_manually
160
    def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
161
        """
162
        Initially creates an inactive user account. If the user is preaccepted
163
        (has a valid invitation code) the user is activated and if the request
164
        param ``next`` is present redirects to it.
165
        In any other case the method returns the action status and a message.
166

167
        The method uses commit_manually decorator in order to ensure the user
168
        will be created only if the procedure has been completed successfully.
169
        """
170
        user = None
171
        try:
172
            user = form.save()
173
            if self._is_preaccepted(user):
174
                if user.email_verified:
175
                    user.is_active = True
176
                    user.save()
177
                    message = _('Registration completed. You can now login.')
178
                else:
179
                    try:
180
                        _send_verification(self.request, user, email_template_name)
181
                        message = _('Verification sent to %s' % user.email)
182
                    except (SMTPException, socket.error) as e:
183
                        status = messages.ERROR
184
                        name = 'strerror'
185
                        message = getattr(e, name) if hasattr(e, name) else e
186
            else:
187
                _send_notification(user, admin_email_template_name)
188
                message = _('Your request for an account was successfully sent \
189
                            and pending approval from our administrators. You \
190
                            will be notified by email the next days. \
191
                            Thanks for being patient, the GRNET team')
192
            status = messages.SUCCESS
193
        except Invitation.DoesNotExist, e:
194
            status = messages.ERROR
195
            message = _('Invalid invitation code')
196
        except socket.error, e:
197
            status = messages.ERROR
198
            message = _(e.strerror)
199

    
200
        # rollback in case of error
201
        if status == messages.ERROR:
202
            transaction.rollback()
203
        else:
204
            transaction.commit()
205
        return status, message, user
206

    
207
class SimpleBackend(SignupBackend):
208
    """
209
    A registration backend which implements the following workflow: a user
210
    supplies the necessary registation information, an incative user account is
211
    created and receives an email in order to activate his/her account.
212
    """
213
    def __init__(self, request):
214
        self.request = request
215
        super(SimpleBackend, self).__init__()
216

    
217
    def get_signup_form(self, provider):
218
        """
219
        Returns the form class name
220
        """
221
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
222
        suffix  = 'UserCreationForm'
223
        formclass = '%s%s' % (main, suffix)
224
        request = self.request
225
        initial_data = None
226
        if request.method == 'POST':
227
            if provider == request.POST.get('provider', ''):
228
                initial_data = request.POST
229
        ip = self.request.META.get('REMOTE_ADDR',
230
                self.request.META.get('HTTP_X_REAL_IP', None))
231
        return globals()[formclass](initial_data, ip=ip)
232
    
233
    def _is_preaccepted(self, user):
234
        if super(SimpleBackend, self)._is_preaccepted(user):
235
            return True
236
        if MODERATION_ENABLED:
237
            return False
238
        return True
239
    
240
    @transaction.commit_manually
241
    def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
242
        """
243
        Creates an inactive user account and sends a verification email.
244

245
        The method uses commit_manually decorator in order to ensure the user
246
        will be created only if the procedure has been completed successfully.
247

248
        ** Arguments **
249

250
        ``email_template_name``
251
            A custom template for the verification email body to use. This is
252
            optional; if not specified, this will default to
253
            ``im/activation_email.txt``.
254

255
        ** Templates **
256
            im/activation_email.txt or ``email_template_name`` keyword argument
257

258
        ** Settings **
259

260
        * DEFAULT_CONTACT_EMAIL: service support email
261
        * DEFAULT_FROM_EMAIL: from email
262
        """
263
        user = form.save()
264
        status = messages.SUCCESS
265
        if not self._is_preaccepted(user):
266
            try:
267
                _send_notification(user, admin_email_template_name)
268
                message = _('Your request for an account was successfully sent \
269
                            and pending approval from our administrators. You \
270
                            will be notified by email the next days. \
271
                            Thanks for being patient, the GRNET team')
272
            except (SMTPException, socket.error) as e:
273
                status = messages.ERROR
274
                name = 'strerror'
275
                message = getattr(e, name) if hasattr(e, name) else e
276
        else:
277
            try:
278
                _send_verification(self.request, user, email_template_name)
279
                message = _('Verification sent to %s' % user.email)
280
            except (SMTPException, socket.error) as e:
281
                status = messages.ERROR
282
                name = 'strerror'
283
                message = getattr(e, name) if hasattr(e, name) else e
284

    
285
        # rollback in case of error
286
        if status == messages.ERROR:
287
            transaction.rollback()
288
        else:
289
            transaction.commit()
290
        return status, message, user
291

    
292
def _send_verification(request, user, template_name):
293
    url = '%s?auth=%s&next=%s' % (urljoin(BASEURL, reverse('astakos.im.views.activate')),
294
                                    quote(user.auth_token),
295
                                    quote(BASEURL))
296
    message = render_to_string(template_name, {
297
            'user': user,
298
            'url': url,
299
            'baseurl': BASEURL,
300
            'site_name': SITENAME,
301
            'support': DEFAULT_CONTACT_EMAIL})
302
    sender = DEFAULT_FROM_EMAIL
303
    send_mail('%s account activation' % SITENAME, message, sender, [user.email])
304
    logger.info('Sent activation %s', user)
305

    
306
def _send_notification(user, template_name):
307
    if not DEFAULT_ADMIN_EMAIL:
308
        return
309
    message = render_to_string(template_name, {
310
            'user': user,
311
            'baseurl': BASEURL,
312
            'site_name': SITENAME,
313
            'support': DEFAULT_CONTACT_EMAIL})
314
    sender = DEFAULT_FROM_EMAIL
315
    send_mail('%s account notification' % SITENAME, message, sender, [DEFAULT_ADMIN_EMAIL])
316
    logger.info('Sent admin notification for user %s', user)