Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / backends.py @ 3a9f4931

History | View | Annotate | Download (11.6 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
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
        ip = self.request.META.get('REMOTE_ADDR',
108
                self.request.META.get('HTTP_X_REAL_IP', None))
109
        return globals()[formclass](initial_data, ip=ip)
110

    
111
    def get_signup_initial_data(self, provider):
112
        """
113
        Returns the necassary registration form depending the user is invited or not
114

115
        Throws Invitation.DoesNotExist in case ``code`` is not valid.
116
        """
117
        request = self.request
118
        invitation = self.invitation
119
        initial_data = None
120
        if request.method == 'GET':
121
            if invitation:
122
                # create a tmp user with the invitation realname
123
                # to extract first and last name
124
                u = AstakosUser(realname = invitation.realname)
125
                initial_data = {'email':invitation.username,
126
                                'inviter':invitation.inviter.realname,
127
                                'first_name':u.first_name,
128
                                'last_name':u.last_name}
129
        else:
130
            if provider == request.POST.get('provider', ''):
131
                initial_data = request.POST
132
        return initial_data
133

    
134
    def _is_preaccepted(self, user):
135
        """
136
        If there is a valid, not-consumed invitation code for the specific user
137
        returns True else returns False.
138
        """
139
        invitation = self.invitation
140
        if not invitation:
141
            return False
142
        if invitation.username == user.email and not invitation.is_consumed:
143
            invitation.consume()
144
            return True
145
        return False
146

    
147
    @transaction.commit_manually
148
    def signup(self, form, admin_email_template_name='im/admin_notification.txt'):
149
        """
150
        Initially creates an inactive user account. If the user is preaccepted
151
        (has a valid invitation code) the user is activated and if the request
152
        param ``next`` is present redirects to it.
153
        In any other case the method returns the action status and a message.
154

155
        The method uses commit_manually decorator in order to ensure the user
156
        will be created only if the procedure has been completed successfully.
157
        """
158
        user = None
159
        try:
160
            user = form.save()
161
            if self._is_preaccepted(user):
162
                user.is_active = True
163
                user.save()
164
                message = _('Registration completed. You can now login.')
165
            else:
166
                _send_notification(user, admin_email_template_name)
167
                message = _('Your request for an account was successfully sent \
168
                            and pending approval from our administrators. You \
169
                            will be notified by email the next days. \
170
                            Thanks for being patient, the GRNET team')
171
            status = messages.SUCCESS
172
        except Invitation.DoesNotExist, e:
173
            status = messages.ERROR
174
            message = _('Invalid invitation code')
175
        except socket.error, e:
176
            status = messages.ERROR
177
            message = _(e.strerror)
178

    
179
        # rollback in case of error
180
        if status == messages.ERROR:
181
            transaction.rollback()
182
        else:
183
            transaction.commit()
184
        return status, message, user
185

    
186
class SimpleBackend(object):
187
    """
188
    A registration backend which implements the following workflow: a user
189
    supplies the necessary registation information, an incative user account is
190
    created and receives an email in order to activate his/her account.
191
    """
192
    def __init__(self, request):
193
        self.request = request
194

    
195
    def get_signup_form(self, provider):
196
        """
197
        Returns the form class name
198
        """
199
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
200
        suffix  = 'UserCreationForm'
201
        formclass = '%s%s' % (main, suffix)
202
        request = self.request
203
        initial_data = None
204
        if request.method == 'POST':
205
            if provider == request.POST.get('provider', ''):
206
                initial_data = request.POST
207
        ip = self.request.META.get('REMOTE_ADDR',
208
                self.request.META.get('HTTP_X_REAL_IP', None))
209
        return globals()[formclass](initial_data, ip=ip)
210

    
211
    @transaction.commit_manually
212
    def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
213
        """
214
        Creates an inactive user account and sends a verification email.
215

216
        The method uses commit_manually decorator in order to ensure the user
217
        will be created only if the procedure has been completed successfully.
218

219
        ** Arguments **
220

221
        ``email_template_name``
222
            A custom template for the verification email body to use. This is
223
            optional; if not specified, this will default to
224
            ``im/activation_email.txt``.
225

226
        ** Templates **
227
            im/activation_email.txt or ``email_template_name`` keyword argument
228

229
        ** Settings **
230

231
        * DEFAULT_CONTACT_EMAIL: service support email
232
        * DEFAULT_FROM_EMAIL: from email
233
        """
234
        user = form.save()
235
        status = messages.SUCCESS
236
        if MODERATION_ENABLED:
237
            try:
238
                _send_notification(user, admin_email_template_name)
239
                message = _('Your request for an account was successfully sent \
240
                            and pending approval from our administrators. You \
241
                            will be notified by email the next days. \
242
                            Thanks for being patient, the GRNET team')
243
            except (SMTPException, socket.error) as e:
244
                status = messages.ERROR
245
                name = 'strerror'
246
                message = getattr(e, name) if hasattr(e, name) else e
247
        else:
248
            try:
249
                _send_verification(self.request, user, email_template_name)
250
                message = _('Verification sent to %s' % user.email)
251
            except (SMTPException, socket.error) as e:
252
                status = messages.ERROR
253
                name = 'strerror'
254
                message = getattr(e, name) if hasattr(e, name) else e
255

    
256
        # rollback in case of error
257
        if status == messages.ERROR:
258
            transaction.rollback()
259
        else:
260
            transaction.commit()
261
        return status, message, user
262

    
263
def _send_verification(request, user, template_name):
264
    url = '%s?auth=%s&next=%s' % (urljoin(BASEURL, reverse('astakos.im.views.activate')),
265
                                    quote(user.auth_token),
266
                                    quote(BASEURL))
267
    message = render_to_string(template_name, {
268
            'user': user,
269
            'url': url,
270
            'baseurl': BASEURL,
271
            'site_name': SITENAME,
272
            'support': DEFAULT_CONTACT_EMAIL})
273
    sender = DEFAULT_FROM_EMAIL
274
    send_mail('%s account activation' % SITENAME, message, sender, [user.email])
275
    logger.info('Sent activation %s', user)
276

    
277
def _send_notification(user, template_name):
278
    if not DEFAULT_ADMIN_EMAIL:
279
        return
280
    message = render_to_string(template_name, {
281
            'user': user,
282
            'baseurl': BASEURL,
283
            'site_name': SITENAME,
284
            'support': DEFAULT_CONTACT_EMAIL})
285
    sender = DEFAULT_FROM_EMAIL
286
    send_mail('%s account notification' % SITENAME, message, sender, [DEFAULT_ADMIN_EMAIL])
287
    logger.info('Sent admin notification for user %s', user)