change signup flow, remove activateuser command and introduce sendactivation command
[astakos] / snf-astakos-app / astakos / im / views.py
1 # Copyright 2011-2012 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 import logging
35 import socket
36
37 from smtplib import SMTPException
38 from urllib import quote
39 from functools import wraps
40
41 from django.core.mail import send_mail
42 from django.http import HttpResponse
43 from django.shortcuts import redirect
44 from django.template.loader import render_to_string
45 from django.utils.translation import ugettext as _
46 from django.core.urlresolvers import reverse
47 from django.contrib.auth.decorators import login_required
48 from django.contrib import messages
49 from django.db import transaction
50 from django.contrib.auth import logout as auth_logout
51 from django.utils.http import urlencode
52 from django.http import HttpResponseRedirect, HttpResponseBadRequest
53 from django.db.utils import IntegrityError
54
55 from astakos.im.models import AstakosUser, Invitation
56 from astakos.im.backends import get_backend
57 from astakos.im.util import get_context, prepare_response, set_cookie
58 from astakos.im.forms import *
59 from astakos.im.functions import send_greeting
60 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
61 from astakos.im.functions import invite as invite_func
62
63 logger = logging.getLogger(__name__)
64
65 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
66     """
67     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
68     keyword argument and returns an ``django.http.HttpResponse`` with the
69     specified ``status``.
70     """
71     if tab is None:
72         tab = template.partition('_')[0].partition('.html')[0]
73     kwargs.setdefault('tab', tab)
74     html = render_to_string(template, kwargs, context_instance=context_instance)
75     response = HttpResponse(html, status=status)
76     if reset_cookie:
77         set_cookie(response, context_instance['request'].user)
78     return response
79
80
81 def requires_anonymous(func):
82     """
83     Decorator checkes whether the request.user is Anonymous and in that case
84     redirects to `logout`.
85     """
86     @wraps(func)
87     def wrapper(request, *args):
88         if not request.user.is_anonymous():
89             next = urlencode({'next': request.build_absolute_uri()})
90             login_uri = reverse(logout) + '?' + next
91             return HttpResponseRedirect(login_uri)
92         return func(request, *args)
93     return wrapper
94
95 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
96     """
97     If there is logged on user renders the profile page otherwise renders login page.
98     
99     **Arguments**
100     
101     ``login_template_name``
102         A custom login template to use. This is optional; if not specified,
103         this will default to ``im/login.html``.
104     
105     ``profile_template_name``
106         A custom profile template to use. This is optional; if not specified,
107         this will default to ``im/profile.html``.
108     
109     ``extra_context``
110         An dictionary of variables to add to the template context.
111     
112     **Template:**
113     
114     im/profile.html or im/login.html or ``template_name`` keyword argument.
115     
116     """
117     template_name = login_template_name
118     formclass = 'LoginForm'
119     kwargs = {}
120     if request.user.is_authenticated():
121         return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
122     return render_response(template_name,
123                            form = globals()[formclass](**kwargs),
124                            context_instance = get_context(request, extra_context))
125
126 @login_required
127 @transaction.commit_manually
128 def invite(request, template_name='im/invitations.html', extra_context={}):
129     """
130     Allows a user to invite somebody else.
131     
132     In case of GET request renders a form for providing the invitee information.
133     In case of POST checks whether the user has not run out of invitations and then
134     sends an invitation email to singup to the service.
135     
136     The view uses commit_manually decorator in order to ensure the number of the
137     user invitations is going to be updated only if the email has been successfully sent.
138     
139     If the user isn't logged in, redirects to settings.LOGIN_URL.
140     
141     **Arguments**
142     
143     ``template_name``
144         A custom template to use. This is optional; if not specified,
145         this will default to ``im/invitations.html``.
146     
147     ``extra_context``
148         An dictionary of variables to add to the template context.
149     
150     **Template:**
151     
152     im/invitations.html or ``template_name`` keyword argument.
153     
154     **Settings:**
155     
156     The view expectes the following settings are defined:
157     
158     * LOGIN_URL: login uri
159     * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
160     * ASTAKOS_DEFAULT_FROM_EMAIL: from email
161     """
162     status = None
163     message = None
164     inviter = AstakosUser.objects.get(username = request.user.username)
165     
166     if request.method == 'POST':
167         username = request.POST.get('uniq')
168         realname = request.POST.get('realname')
169         
170         if inviter.invitations > 0:
171             try:
172                 invite_func(inviter, username, realname)
173                 status = messages.SUCCESS
174                 message = _('Invitation sent to %s' % username)
175                 transaction.commit()
176             except (SMTPException, socket.error) as e:
177                 status = messages.ERROR
178                 message = getattr(e, 'strerror', '')
179                 transaction.rollback()
180             except IntegrityError, e:
181                 status = messages.ERROR
182                 message = _('There is already invitation for %s' % username)
183                 transaction.rollback()
184         else:
185             status = messages.ERROR
186             message = _('No invitations left')
187     messages.add_message(request, status, message)
188     
189     sent = [{'email': inv.username,
190              'realname': inv.realname,
191              'is_consumed': inv.is_consumed}
192              for inv in inviter.invitations_sent.all()]
193     kwargs = {'inviter': inviter,
194               'sent':sent}
195     context = get_context(request, extra_context, **kwargs)
196     return render_response(template_name,
197                            context_instance = context)
198
199 @login_required
200 def edit_profile(request, template_name='im/profile.html', extra_context={}):
201     """
202     Allows a user to edit his/her profile.
203     
204     In case of GET request renders a form for displaying the user information.
205     In case of POST updates the user informantion and redirects to ``next``
206     url parameter if exists.
207     
208     If the user isn't logged in, redirects to settings.LOGIN_URL.
209     
210     **Arguments**
211     
212     ``template_name``
213         A custom template to use. This is optional; if not specified,
214         this will default to ``im/profile.html``.
215     
216     ``extra_context``
217         An dictionary of variables to add to the template context.
218     
219     **Template:**
220     
221     im/profile.html or ``template_name`` keyword argument.
222     
223     **Settings:**
224     
225     The view expectes the following settings are defined:
226     
227     * LOGIN_URL: login uri
228     """
229     form = ProfileForm(instance=request.user)
230     extra_context['next'] = request.GET.get('next')
231     reset_cookie = False
232     if request.method == 'POST':
233         form = ProfileForm(request.POST, instance=request.user)
234         if form.is_valid():
235             try:
236                 prev_token = request.user.auth_token
237                 user = form.save()
238                 reset_cookie = user.auth_token != prev_token
239                 form = ProfileForm(instance=user)
240                 next = request.POST.get('next')
241                 if next:
242                     return redirect(next)
243                 msg = _('Profile has been updated successfully')
244                 messages.add_message(request, messages.SUCCESS, msg)
245             except ValueError, ve:
246                 messages.add_message(request, messages.ERROR, ve)
247     return render_response(template_name,
248                            reset_cookie = reset_cookie,
249                            form = form,
250                            context_instance = get_context(request,
251                                                           extra_context))
252
253 def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
254     """
255     Allows a user to create a local account.
256     
257     In case of GET request renders a form for providing the user information.
258     In case of POST handles the signup.
259     
260     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
261     if present, otherwise to the ``astakos.im.backends.InvitationBackend``
262     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
263     (see backends);
264     
265     Upon successful user creation if ``next`` url parameter is present the user is redirected there
266     otherwise renders the same page with a success message.
267     
268     On unsuccessful creation, renders ``on_failure`` with an error message.
269     
270     **Arguments**
271     
272     ``on_failure``
273         A custom template to render in case of failure. This is optional;
274         if not specified, this will default to ``im/signup.html``.
275     
276     
277     ``on_success``
278         A custom template to render in case of success. This is optional;
279         if not specified, this will default to ``im/signup_complete.html``.
280     
281     ``extra_context``
282         An dictionary of variables to add to the template context.
283     
284     **Template:**
285     
286     im/signup.html or ``on_failure`` keyword argument.
287     im/signup_complete.html or ``on_success`` keyword argument. 
288     """
289     if request.user.is_authenticated():
290         return HttpResponseRedirect(reverse('astakos.im.views.index'))
291     try:
292         if not backend:
293             backend = get_backend(request)
294         for provider in IM_MODULES:
295             extra_context['%s_form' % provider] = backend.get_signup_form(provider)
296         if request.method == 'POST':
297             provider = request.POST.get('provider')
298             next = request.POST.get('next', '')
299             form = extra_context['%s_form' % provider]
300             if form.is_valid():
301                 if provider != 'local':
302                     url = reverse('astakos.im.target.%s.login' % provider)
303                     url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
304                     if backend.invitation:
305                         url = '%s&code=%s' % (url, backend.invitation.code)
306                     return redirect(url)
307                 else:
308                     status, message, user = backend.signup(form)
309                     if user and user.is_active:
310                         return prepare_response(request, user, next=next)
311                     messages.add_message(request, status, message)
312                     return render_response(on_success,
313                                            context_instance=get_context(request, extra_context))
314     except (Invitation.DoesNotExist, ValueError), e:
315         messages.add_message(request, messages.ERROR, e)
316         for provider in IM_MODULES:
317             main = provider.capitalize() if provider == 'local' else 'ThirdParty'
318             formclass = '%sUserCreationForm' % main
319             extra_context['%s_form' % provider] = globals()[formclass]()
320     return render_response(on_failure,
321                            context_instance=get_context(request, extra_context))
322
323 @login_required
324 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
325     """
326     Allows a user to send feedback.
327     
328     In case of GET request renders a form for providing the feedback information.
329     In case of POST sends an email to support team.
330     
331     If the user isn't logged in, redirects to settings.LOGIN_URL.
332     
333     **Arguments**
334     
335     ``template_name``
336         A custom template to use. This is optional; if not specified,
337         this will default to ``im/feedback.html``.
338     
339     ``extra_context``
340         An dictionary of variables to add to the template context.
341     
342     **Template:**
343     
344     im/signup.html or ``template_name`` keyword argument.
345     
346     **Settings:**
347     
348     * LOGIN_URL: login uri
349     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
350     """
351     if request.method == 'GET':
352         form = FeedbackForm()
353     if request.method == 'POST':
354         if not request.user:
355             return HttpResponse('Unauthorized', status=401)
356         
357         form = FeedbackForm(request.POST)
358         if form.is_valid():
359             subject = _("Feedback from %s alpha2 testing" % SITENAME)
360             from_email = request.user.email
361             recipient_list = [DEFAULT_CONTACT_EMAIL]
362             content = render_to_string(email_template_name, {
363                         'message': form.cleaned_data['feedback_msg'],
364                         'data': form.cleaned_data['feedback_data'],
365                         'request': request})
366             
367             try:
368                 send_mail(subject, content, from_email, recipient_list)
369                 message = _('Feedback successfully sent')
370                 status = messages.SUCCESS
371             except (SMTPException, socket.error) as e:
372                 status = messages.ERROR
373                 message = getattr(e, 'strerror', '')
374             messages.add_message(request, status, message)
375     return render_response(template_name,
376                            form = form,
377                            context_instance = get_context(request, extra_context))
378
379 def logout(request, template='registration/logged_out.html', extra_context={}):
380     """
381     Wraps `django.contrib.auth.logout` and delete the cookie.
382     """
383     auth_logout(request)
384     response = HttpResponse()
385     response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
386     next = request.GET.get('next')
387     if next:
388         response['Location'] = next
389         response.status_code = 302
390         return response
391     elif LOGOUT_NEXT:
392         response['Location'] = LOGOUT_NEXT
393         response.status_code = 301
394         return response
395     messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
396     context = get_context(request, extra_context)
397     response.write(render_to_string(template, context_instance=context))
398     return response
399
400 @transaction.commit_manually
401 def activate(request, email_template_name='im/welcome_email.txt', on_failure=''):
402     """
403     Activates the user identified by the ``auth`` request parameter, sends a welcome email
404     and renews the user token.
405     
406     The view uses commit_manually decorator in order to ensure the user state will be updated
407     only if the email will be send successfully.
408     """
409     token = request.GET.get('auth')
410     next = request.GET.get('next')
411     try:
412         user = AstakosUser.objects.get(auth_token=token)
413     except AstakosUser.DoesNotExist:
414         return HttpResponseBadRequest(_('No such user'))
415     
416     user.is_active = True
417     user.email_verified = True
418     user.save()
419     try:
420         send_greeting(user, email_template_name)
421         response = prepare_response(request, user, next, renew=True)
422         transaction.commit()
423         return response
424     except (SMTPException, socket.error) as e:
425         message = getattr(e, 'name') if hasattr(e, 'name') else e
426         messages.add_message(request, messages.ERROR, message)
427         transaction.rollback()
428         return signup(request, on_failure='im/signup.html')