12e703737e0d72ff4646289d99790cae25c4cfda
[astakos] / snf-astakos-app / astakos / im / backends.py
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)