changes in cloudier and signup & logout behavior
[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
53
54 from astakos.im.models import AstakosUser, Invitation
55 from astakos.im.backends import get_backend
56 from astakos.im.util import get_context, prepare_response, set_cookie
57 from astakos.im.forms import *
58 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
59 from astakos.im.functions import invite as invite_func
60
61 logger = logging.getLogger(__name__)
62
63 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
64     """
65     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
66     keyword argument and returns an ``django.http.HttpResponse`` with the
67     specified ``status``.
68     """
69     if tab is None:
70         tab = template.partition('_')[0].partition('.html')[0]
71     kwargs.setdefault('tab', tab)
72     html = render_to_string(template, kwargs, context_instance=context_instance)
73     response = HttpResponse(html, status=status)
74     if reset_cookie:
75         set_cookie(response, context_instance['request'].user)
76     return response
77
78
79 def requires_anonymous(func):
80     """
81     Decorator checkes whether the request.user is Anonymous and in that case
82     redirects to `logout`.
83     """
84     @wraps(func)
85     def wrapper(request, *args):
86         if not request.user.is_anonymous():
87             next = urlencode({'next': request.build_absolute_uri()})
88             login_uri = reverse(logout) + '?' + next
89             return HttpResponseRedirect(login_uri)
90         return func(request, *args)
91     return wrapper
92
93 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
94     """
95     If there is logged on user renders the profile page otherwise renders login page.
96     
97     **Arguments**
98     
99     ``login_template_name``
100         A custom login template to use. This is optional; if not specified,
101         this will default to ``im/login.html``.
102     
103     ``profile_template_name``
104         A custom profile template to use. This is optional; if not specified,
105         this will default to ``im/profile.html``.
106     
107     ``extra_context``
108         An dictionary of variables to add to the template context.
109     
110     **Template:**
111     
112     im/profile.html or im/login.html or ``template_name`` keyword argument.
113     
114     """
115     template_name = login_template_name
116     formclass = 'LoginForm'
117     kwargs = {}
118     if request.user.is_authenticated():
119         template_name = profile_template_name
120         formclass = 'ProfileForm'
121         kwargs.update({'instance':request.user})
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         else:
181             status = messages.ERROR
182             message = _('No invitations left')
183     messages.add_message(request, status, message)
184     
185     sent = [{'email': inv.username,
186              'realname': inv.realname,
187              'is_consumed': inv.is_consumed}
188              for inv in inviter.invitations_sent.all()]
189     kwargs = {'inviter': inviter,
190               'sent':sent}
191     context = get_context(request, extra_context, **kwargs)
192     return render_response(template_name,
193                            context_instance = context)
194
195 @login_required
196 def edit_profile(request, template_name='im/profile.html', extra_context={}):
197     """
198     Allows a user to edit his/her profile.
199     
200     In case of GET request renders a form for displaying the user information.
201     In case of POST updates the user informantion and redirects to ``next``
202     url parameter if exists.
203     
204     If the user isn't logged in, redirects to settings.LOGIN_URL.
205     
206     **Arguments**
207     
208     ``template_name``
209         A custom template to use. This is optional; if not specified,
210         this will default to ``im/profile.html``.
211     
212     ``extra_context``
213         An dictionary of variables to add to the template context.
214     
215     **Template:**
216     
217     im/profile.html or ``template_name`` keyword argument.
218     
219     **Settings:**
220     
221     The view expectes the following settings are defined:
222     
223     * LOGIN_URL: login uri
224     """
225     form = ProfileForm(instance=request.user)
226     extra_context['next'] = request.GET.get('next')
227     reset_cookie = False
228     if request.method == 'POST':
229         form = ProfileForm(request.POST, instance=request.user)
230         if form.is_valid():
231             try:
232                 prev_token = request.user.auth_token
233                 user = form.save()
234                 reset_cookie = user.auth_token != prev_token
235                 form = ProfileForm(instance=user)
236                 next = request.POST.get('next')
237                 if next:
238                     return redirect(next)
239                 msg = _('Profile has been updated successfully')
240                 messages.add_message(request, messages.SUCCESS, msg)
241             except ValueError, ve:
242                 messages.add_message(request, messages.ERROR, ve)
243     return render_response(template_name,
244                            reset_cookie = reset_cookie,
245                            form = form,
246                            context_instance = get_context(request,
247                                                           extra_context))
248
249 def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
250     """
251     Allows a user to create a local account.
252     
253     In case of GET request renders a form for providing the user information.
254     In case of POST handles the signup.
255     
256     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
257     if present, otherwise to the ``astakos.im.backends.InvitationBackend``
258     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
259     (see backends);
260     
261     Upon successful user creation if ``next`` url parameter is present the user is redirected there
262     otherwise renders the same page with a success message.
263     
264     On unsuccessful creation, renders the same page with an error message.
265     
266     **Arguments**
267     
268     ``on_failure``
269         A custom template to render in case of failure. This is optional;
270         if not specified, this will default to ``im/signup.html``.
271     
272     
273     ``on_success``
274         A custom template to render in case of success. This is optional;
275         if not specified, this will default to ``im/signup_complete.html``.
276     
277     ``extra_context``
278         An dictionary of variables to add to the template context.
279     
280     **Template:**
281     
282     im/signup.html or ``on_failure`` keyword argument.
283     im/signup_complete.html or ``on_success`` keyword argument. 
284     """
285     if request.user.is_authenticated():
286         return HttpResponseRedirect(reverse('astakos.im.views.index'))
287     try:
288         if not backend:
289             backend = get_backend(request)
290         for provider in IM_MODULES:
291             extra_context['%s_form' % provider] = backend.get_signup_form(provider)
292         if request.method == 'POST':
293             provider = request.POST.get('provider')
294             next = request.POST.get('next', '')
295             form = extra_context['%s_form' % provider]
296             if form.is_valid():
297                 if provider != 'local':
298                     url = reverse('astakos.im.target.%s.login' % provider)
299                     url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
300                     if backend.invitation:
301                         url = '%s&code=%s' % (url, backend.invitation.code)
302                     return redirect(url)
303                 else:
304                     status, message, user = backend.signup(form)
305                     if user and user.is_active:
306                         return prepare_response(request, user, next=next)
307                     messages.add_message(request, status, message)
308                     return render_response(on_success,
309                                            context_instance=get_context(request, extra_context))
310     except (Invitation.DoesNotExist, ValueError), e:
311         messages.add_message(request, messages.ERROR, e)
312         for provider in IM_MODULES:
313             main = provider.capitalize() if provider == 'local' else 'ThirdParty'
314             formclass = '%sUserCreationForm' % main
315             extra_context['%s_form' % provider] = globals()[formclass]()
316     return render_response(on_failure,
317                            context_instance=get_context(request, extra_context))
318
319 @login_required
320 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
321     """
322     Allows a user to send feedback.
323     
324     In case of GET request renders a form for providing the feedback information.
325     In case of POST sends an email to support team.
326     
327     If the user isn't logged in, redirects to settings.LOGIN_URL.
328     
329     **Arguments**
330     
331     ``template_name``
332         A custom template to use. This is optional; if not specified,
333         this will default to ``im/feedback.html``.
334     
335     ``extra_context``
336         An dictionary of variables to add to the template context.
337     
338     **Template:**
339     
340     im/signup.html or ``template_name`` keyword argument.
341     
342     **Settings:**
343     
344     * LOGIN_URL: login uri
345     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
346     """
347     if request.method == 'GET':
348         form = FeedbackForm()
349     if request.method == 'POST':
350         if not request.user:
351             return HttpResponse('Unauthorized', status=401)
352         
353         form = FeedbackForm(request.POST)
354         if form.is_valid():
355             subject = _("Feedback from %s" % SITENAME)
356             from_email = request.user.email
357             recipient_list = [DEFAULT_CONTACT_EMAIL]
358             content = render_to_string(email_template_name, {
359                         'message': form.cleaned_data['feedback_msg'],
360                         'data': form.cleaned_data['feedback_data'],
361                         'request': request})
362             
363             try:
364                 send_mail(subject, content, from_email, recipient_list)
365                 message = _('Feedback successfully sent')
366                 status = messages.SUCCESS
367             except (SMTPException, socket.error) as e:
368                 status = messages.ERROR
369                 message = getattr(e, 'strerror', '')
370             messages.add_message(request, status, message)
371     return render_response(template_name,
372                            form = form,
373                            context_instance = get_context(request, extra_context))
374
375 def logout(request, template='registration/logged_out.html', extra_context={}):
376     """
377     Wraps `django.contrib.auth.logout` and delete the cookie.
378     """
379     auth_logout(request)
380     response = HttpResponse()
381     response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
382     next = request.GET.get('next')
383     if next:
384         response['Location'] = next
385         response.status_code = 302
386         return response
387     elif LOGOUT_NEXT:
388         response['Location'] = LOGOUT_NEXT
389         response.status_code = 301
390         return response
391     messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
392     context = get_context(request, extra_context)
393     response.write(render_to_string(template, context_instance=context))
394     return response
395
396 def activate(request):
397     """
398     Activates the user identified by the ``auth`` request parameter
399     """
400     token = request.GET.get('auth')
401     next = request.GET.get('next')
402     try:
403         user = AstakosUser.objects.get(auth_token=token)
404     except AstakosUser.DoesNotExist:
405         return HttpResponseBadRequest('No such user')
406     
407     user.is_active = True
408     user.save()
409     return prepare_response(request, user, next, renew=True)