Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (11.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, 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 = _('Registration completed. You will receive an email upon your account\'s activation.')
168
            status = messages.SUCCESS
169
        except Invitation.DoesNotExist, e:
170
            status = messages.ERROR
171
            message = _('Invalid invitation code')
172
        except socket.error, e:
173
            status = messages.ERROR
174
            message = _(e.strerror)
175

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

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

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

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

213
        The method uses commit_manually decorator in order to ensure the user
214
        will be created only if the procedure has been completed successfully.
215

216
        ** Arguments **
217

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

223
        ** Templates **
224
            im/activation_email.txt or ``email_template_name`` keyword argument
225

226
        ** Settings **
227

228
        * DEFAULT_CONTACT_EMAIL: service support email
229
        * DEFAULT_FROM_EMAIL: from email
230
        """
231
        user = form.save()
232
        status = messages.SUCCESS
233
        if MODERATION_ENABLED:
234
            try:
235
                _send_notification(user, admin_email_template_name)
236
                message = _('Registration completed. You will receive an email upon your account\'s activation.')
237
            except (SMTPException, socket.error) as e:
238
                status = messages.ERROR
239
                name = 'strerror'
240
                message = getattr(e, name) if hasattr(e, name) else e
241
        else:
242
            try:
243
                _send_verification(self.request, user, email_template_name)
244
                message = _('Verification sent to %s' % user.email)
245
            except (SMTPException, socket.error) as e:
246
                status = messages.ERROR
247
                name = 'strerror'
248
                message = getattr(e, name) if hasattr(e, name) else e
249

    
250
        # rollback in case of error
251
        if status == messages.ERROR:
252
            transaction.rollback()
253
        else:
254
            transaction.commit()
255
        return status, message, user
256

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

    
271
def _send_notification(user, template_name):
272
    if not DEFAULT_ADMIN_EMAIL:
273
        return
274
    message = render_to_string(template_name, {
275
            'user': user,
276
            'baseurl': BASEURL,
277
            'site_name': SITENAME,
278
            'support': DEFAULT_CONTACT_EMAIL})
279
    sender = DEFAULT_FROM_EMAIL
280
    send_mail('%s account notification' % SITENAME, message, sender, [DEFAULT_ADMIN_EMAIL])
281
    logger.info('Sent admin notification for user %s', user)