2b3b03a9253b7841448fa6a2f972a6de78466a77
[astakos] / astakos / im / backends / __init__.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.conf import settings
35 from django.utils.importlib import import_module
36 from django.core.exceptions import ImproperlyConfigured
37 from django.core.mail import send_mail
38 from django.template.loader import render_to_string
39 from django.utils.translation import ugettext as _
40 from django.contrib.auth.forms import UserCreationForm
41 from django.contrib.sites.models import Site
42 from django.contrib import messages
43 from django.shortcuts import redirect
44
45 from smtplib import SMTPException
46 from urllib import quote
47
48 from astakos.im.util import get_or_create_user
49 from astakos.im.models import AstakosUser, Invitation
50 from astakos.im.forms import ExtendedUserCreationForm, InvitedExtendedUserCreationForm
51
52 import socket
53 import logging
54
55 def get_backend(request):
56     """
57     Returns an instance of a registration backend,
58     according to the INVITATIONS_ENABLED setting
59     (if True returns ``astakos.im.backends.InvitationsBackend`` and if False
60     returns ``astakos.im.backends.SimpleBackend``).
61     
62     If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
63     is raised.
64     """
65     module = 'astakos.im.backends'
66     prefix = 'Invitations' if settings.INVITATIONS_ENABLED else 'Simple'
67     backend_class_name = '%sBackend' %prefix
68     try:
69         mod = import_module(module)
70     except ImportError, e:
71         raise ImproperlyConfigured('Error loading registration backend %s: "%s"' % (module, e))
72     try:
73         backend_class = getattr(mod, backend_class_name)
74     except AttributeError:
75         raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
76     return backend_class(request)
77
78 class InvitationsBackend(object):
79     """
80     A registration backend which implements the following workflow: a user
81     supplies the necessary registation information, if the request contains a valid
82     inivation code the user is automatically activated otherwise an inactive user
83     account is created and the user is going to receive an email as soon as an
84     administrator activates his/her account.
85     """
86     def __init__(self, request):
87         self.request = request
88         self.invitation = None
89         self.set_invitation()
90     
91     def set_invitation(self):
92         code = self.request.GET.get('code', '')
93         if not code:
94             code = self.request.POST.get('code', '')
95         if code:
96             self.invitation = Invitation.objects.get(code=code)
97             if self.invitation.is_consumed:
98                     raise Exception('Invitation has beeen used')
99     
100     def get_signup_form(self):
101         """
102         Returns the necassary registration form depending the user is invited or not
103         
104         Throws Invitation.DoesNotExist in case ``code`` is not valid.
105         """
106         request = self.request
107         formclass = 'ExtendedUserCreationForm'
108         if self.invitation:
109             formclass = 'Invited%s' %formclass
110         initial_data = None
111         if request.method == 'GET':
112             if self.invitation:
113                 # create a tmp user with the invitation realname
114                 # to extract first and last name
115                 u = AstakosUser(realname = self.invitation.realname)
116                 initial_data = {'username':self.invitation.username,
117                                 'email':self.invitation.username,
118                                 'inviter':self.invitation.inviter.realname,
119                                 'first_name':u.first_name,
120                                 'last_name':u.last_name}
121         elif request.method == 'POST':
122             initial_data = request.POST
123         return globals()[formclass](initial_data)
124     
125     def _is_preaccepted(self, user):
126         """
127         If there is a valid, not-consumed invitation code for the specific user
128         returns True else returns False.
129         
130         It should be called after ``get_signup_form`` which sets invitation if exists.
131         """
132         invitation = self.invitation
133         if not invitation:
134             return False
135         if invitation.username == user.email and not invitation.is_consumed:
136             invitation.consume()
137             return True
138         return False
139     
140     def signup(self, form):
141         """
142         Initially creates an inactive user account. If the user is preaccepted
143         (has a valid invitation code) the user is activated and if the request
144         param ``next`` is present redirects to it.
145         In any other case the method returns the action status and a message.
146         """
147         kwargs = {}
148         user = form.save()
149         try:
150             if self._is_preaccepted(user):
151                 user.is_active = True
152                 user.save()
153                 message = _('Registration completed. You can now login.')
154             else:
155                 message = _('Registration completed. You will receive an email upon your account\'s activation')
156             status = messages.SUCCESS
157         except Invitation.DoesNotExist, e:
158             status = messages.ERROR
159             message = _('Invalid invitation code')
160         return status, message
161
162 class SimpleBackend(object):
163     """
164     A registration backend which implements the following workflow: a user
165     supplies the necessary registation information, an incative user account is
166     created and receives an email in order to activate his/her account.
167     """
168     def __init__(self, request):
169         self.request = request
170     
171     def get_signup_form(self):
172         """
173         Returns the UserCreationForm
174         """
175         request = self.request
176         initial_data = request.POST if request.method == 'POST' else None
177         return ExtendedUserCreationForm(initial_data)
178     
179     def signup(self, form, email_template_name='activation_email.txt'):
180         """
181         Creates an inactive user account and sends a verification email.
182         
183         ** Arguments **
184         
185         ``email_template_name``
186             A custom template for the verification email body to use. This is
187             optional; if not specified, this will default to
188             ``activation_email.txt``.
189         
190         ** Templates **
191             activation_email.txt or ``email_template_name`` keyword argument
192         
193         ** Settings **
194         
195         * ACTIVATION_LOGIN_TARGET: Where users should activate their local account
196         * DEFAULT_CONTACT_EMAIL: service support email
197         * DEFAULT_FROM_EMAIL: from email
198         """
199         kwargs = {}
200         user = form.save()
201         
202         status = messages.SUCCESS
203         try:
204             _send_verification(self.request, user, email_template_name)
205             message = _('Verification sent to %s' % user.email)
206         except (SMTPException, socket.error) as e:
207             status = messages.ERROR
208             name = 'strerror'
209             message = getattr(e, name) if hasattr(e, name) else e
210         return status, message
211
212 def _send_verification(request, user, template_name):
213     site = Site.objects.get_current()
214     baseurl = request.build_absolute_uri('/').rstrip('/')
215     url = settings.ACTIVATION_LOGIN_TARGET % (baseurl,
216                                               quote(user.auth_token),
217                                               quote(baseurl))
218     message = render_to_string(template_name, {
219             'user': user,
220             'url': url,
221             'baseurl': baseurl,
222             'site_name': site.name,
223             'support': settings.DEFAULT_CONTACT_EMAIL % site.name.lower()})
224     sender = settings.DEFAULT_FROM_EMAIL % site.name
225     send_mail('%s account activation' % site.name, message, sender, [user.email])
226     logging.info('Sent activation %s', user)