Update docstring for activation backends
[astakos] / snf-astakos-app / astakos / im / activation_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.contrib import messages
39 from django.core.urlresolvers import reverse
40 from django.utils.translation import ugettext as _
41 from django.db import transaction
42
43 from urlparse import urljoin
44
45 from astakos.im.models import AstakosUser, Invitation
46 from astakos.im.forms import *
47 from astakos.im.util import get_invitation
48 from astakos.im.functions import send_verification, send_activation, \
49     send_admin_notification, activate, SendMailError
50 from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, \
51     DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, 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 an activation backend,
62     according to the INVITATIONS_ENABLED setting
63     (if True returns ``astakos.im.activation_backends.InvitationsBackend`` and if False
64     returns ``astakos.im.activation_backends.SimpleBackend``).
65
66     If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
67     is raised.
68     """
69     module = 'astakos.im.activation_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 activation 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 activation backend named "%s"' % (module, attr))
80     return backend_class(request)
81
82 class ActivationBackend(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     def get_signup_form(self, provider='local', instance=None):
91         """
92         Returns a form instance of the relevant class
93         """
94         main = provider.capitalize() if provider == 'local' else 'ThirdParty'
95         suffix  = 'UserCreationForm'
96         formclass = '%s%s' % (main, suffix)
97         request = self.request
98         initial_data = None
99         if request.method == 'POST':
100             if provider == request.POST.get('provider', ''):
101                 initial_data = request.POST
102         return globals()[formclass](initial_data, instance=instance, request=request)
103     
104     def handle_activation(
105         self, user, activation_template_name='im/activation_email.txt',
106         greeting_template_name='im/welcome_email.txt',
107         admin_email_template_name='im/admin_notification.txt'
108     ):
109         """
110         If the user is already active returns immediately.
111         If the user is preaccepted and the email is verified, the account is
112         activated automatically. Otherwise, if the email is not verified,
113         it sends a verification email to the user.
114         If the user is not preaccepted, it sends an email to the administrators
115         and informs the user that the account is pending activation.
116         """
117         try:
118             if user.is_active:
119                 return RegistationCompleted()
120             
121             if self._is_preaccepted(user):
122                 if user.email_verified:
123                     activate(user, greeting_template_name)
124                     return RegistationCompleted()
125                 else:
126                     send_activation(user, activation_template_name)
127                     return VerificationSent()
128             else:
129                 send_admin_notification(user, admin_email_template_name)
130                 return NotificationSent()
131         except BaseException, e:
132             logger.exception(e)
133             raise e
134
135 class InvitationsBackend(ActivationBackend):
136     """
137     A activation backend which implements the following workflow: a user
138     supplies the necessary registation information, if the request contains a valid
139     inivation code the user is automatically activated otherwise an inactive user
140     account is created and the user is going to receive an email as soon as an
141     administrator activates his/her account.
142     """
143     def __init__(self, request):
144         self.request = request
145         super(InvitationsBackend, self).__init__()
146
147     def get_signup_form(self, provider='local', instance=None):
148         """
149         Returns a form instance of the relevant class
150         
151         raises Invitation.DoesNotExist and ValueError if invitation is consumed
152         or invitation username is reserved.
153         """
154         self.invitation = get_invitation(self.request)
155         invitation = self.invitation
156         initial_data = self.get_signup_initial_data(provider)
157         prefix = 'Invited' if invitation else ''
158         main = provider.capitalize()
159         suffix  = 'UserCreationForm'
160         formclass = '%s%s%s' % (prefix, main, suffix)
161         return globals()[formclass](initial_data, instance=instance, request=self.request)
162
163     def get_signup_initial_data(self, provider):
164         """
165         Returns the necassary activation form depending the user is invited or not
166
167         Throws Invitation.DoesNotExist in case ``code`` is not valid.
168         """
169         request = self.request
170         invitation = self.invitation
171         initial_data = None
172         if request.method == 'GET':
173             if invitation:
174                 # create a tmp user with the invitation realname
175                 # to extract first and last name
176                 u = AstakosUser(realname = invitation.realname)
177                 initial_data = {'email':invitation.username,
178                                 'inviter':invitation.inviter.realname,
179                                 'first_name':u.first_name,
180                                 'last_name':u.last_name,
181                                 'provider':provider}
182         else:
183             if provider == request.POST.get('provider', ''):
184                 initial_data = request.POST
185         return initial_data
186
187     def _is_preaccepted(self, user):
188         """
189         Extends _is_preaccepted and if there is a valid, not-consumed invitation
190         code for the specific user returns True else returns False.
191         """
192         if super(InvitationsBackend, self)._is_preaccepted(user):
193             return True
194         invitation = self.invitation
195         if not invitation:
196             return False
197         if invitation.username == user.email and not invitation.is_consumed:
198             invitation.consume()
199             return True
200         return False
201
202 class SimpleBackend(ActivationBackend):
203     """
204     A activation backend which implements the following workflow: a user
205     supplies the necessary registation information, an incative user account is
206     created and receives an email in order to activate his/her account.
207     """
208     def __init__(self, request):
209         self.request = request
210         super(SimpleBackend, self).__init__()
211     
212     def _is_preaccepted(self, user):
213         if super(SimpleBackend, self)._is_preaccepted(user):
214             return True
215         if MODERATION_ENABLED:
216             return False
217         return True
218
219 class ActivationResult(object):
220     def __init__(self, message):
221         self.message = message
222
223 class VerificationSent(ActivationResult):
224     def __init__(self):
225         message = _('Verification sent.')
226         super(VerificationSent, self).__init__(message)
227
228 class NotificationSent(ActivationResult):
229     def __init__(self):
230         message = _('Your request for an account was successfully received and is now pending \
231                     approval. You will be notified by email in the next few days. Thanks for \
232                     your interest in ~okeanos! The GRNET team.')
233         super(NotificationSent, self).__init__(message)
234
235 class RegistationCompleted(ActivationResult):
236     def __init__(self):
237         message = _('Registration completed. You can now login.')
238         super(RegistationCompleted, self).__init__(message)