1 # Copyright 2011 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
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
44 from smtplib import SMTPException
45 from urllib import quote
46 from urlparse import urljoin
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
56 logger = logging.getLogger(__name__)
58 def get_backend(request):
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``).
65 If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
68 module = 'astakos.im.backends'
69 prefix = 'Invitations' if INVITATIONS_ENABLED else 'Simple'
70 backend_class_name = '%sBackend' %prefix
72 mod = import_module(module)
73 except ImportError, e:
74 raise ImproperlyConfigured('Error loading registration backend %s: "%s"' % (module, e))
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)
81 class InvitationsBackend(object):
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.
89 def __init__(self, request):
91 raises Invitation.DoesNotExist and ValueError if invitation is consumed
92 or invitation username is reserved.
94 self.request = request
95 self.invitation = get_invitation(request)
97 def get_signup_form(self, provider):
99 Returns the form class name
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)
111 def get_signup_initial_data(self, provider):
113 Returns the necassary registration form depending the user is invited or not
115 Throws Invitation.DoesNotExist in case ``code`` is not valid.
117 request = self.request
118 invitation = self.invitation
120 if request.method == 'GET':
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}
130 if provider == request.POST.get('provider', ''):
131 initial_data = request.POST
134 def _is_preaccepted(self, user):
136 If there is a valid, not-consumed invitation code for the specific user
137 returns True else returns False.
139 invitation = self.invitation
142 if invitation.username == user.email and not invitation.is_consumed:
147 @transaction.commit_manually
148 def signup(self, form, admin_email_template_name='im/admin_notification.txt'):
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.
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.
161 if self._is_preaccepted(user):
162 user.is_active = True
164 message = _('Registration completed. You can now login.')
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)
179 # rollback in case of error
180 if status == messages.ERROR:
181 transaction.rollback()
184 return status, message, user
186 class SimpleBackend(object):
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.
192 def __init__(self, request):
193 self.request = request
195 def get_signup_form(self, provider):
197 Returns the form class name
199 main = provider.capitalize() if provider == 'local' else 'ThirdParty'
200 suffix = 'UserCreationForm'
201 formclass = '%s%s' % (main, suffix)
202 request = self.request
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)
211 @transaction.commit_manually
212 def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
214 Creates an inactive user account and sends a verification email.
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.
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``.
227 im/activation_email.txt or ``email_template_name`` keyword argument
231 * DEFAULT_CONTACT_EMAIL: service support email
232 * DEFAULT_FROM_EMAIL: from email
235 status = messages.SUCCESS
236 if MODERATION_ENABLED:
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
246 message = getattr(e, name) if hasattr(e, name) else e
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
254 message = getattr(e, name) if hasattr(e, name) else e
256 # rollback in case of error
257 if status == messages.ERROR:
258 transaction.rollback()
261 return status, message, user
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),
267 message = render_to_string(template_name, {
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)
277 def _send_notification(user, template_name):
278 if not DEFAULT_ADMIN_EMAIL:
280 message = render_to_string(template_name, {
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)