Repo refactor and webproject hooks fixes
[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
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 @requires_anonymous
250 def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
251     """
252     Allows a user to create a local account.
253     
254     In case of GET request renders a form for providing the user information.
255     In case of POST handles the signup.
256     
257     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
258     if present, otherwise to the ``astakos.im.backends.InvitationBackend``
259     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
260     (see backends);
261     
262     Upon successful user creation if ``next`` url parameter is present the user is redirected there
263     otherwise renders the same page with a success message.
264     
265     On unsuccessful creation, renders the same page with an error message.
266     
267     **Arguments**
268     
269     ``on_failure``
270         A custom template to render in case of failure. This is optional;
271         if not specified, this will default to ``im/signup.html``.
272     
273     
274     ``on_success``
275         A custom template to render in case of success. This is optional;
276         if not specified, this will default to ``im/signup_complete.html``.
277     
278     ``extra_context``
279         An dictionary of variables to add to the template context.
280     
281     **Template:**
282     
283     im/signup.html or ``on_failure`` keyword argument.
284     im/signup_complete.html or ``on_success`` keyword argument. 
285     """
286     try:
287         if not backend:
288             backend = get_backend(request)
289         for provider in IM_MODULES:
290             extra_context['%s_form' % provider] = backend.get_signup_form(provider)
291         if request.method == 'POST':
292             provider = request.POST.get('provider')
293             next = request.POST.get('next', '')
294             form = extra_context['%s_form' % provider]
295             if form.is_valid():
296                 if provider != 'local':
297                     url = reverse('astakos.im.target.%s.login' % provider)
298                     url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
299                     if backend.invitation:
300                         url = '%s&code=%s' % (url, backend.invitation.code)
301                     return redirect(url)
302                 else:
303                     status, message, user = backend.signup(form)
304                     if user and user.is_active:
305                         return prepare_response(request, user, next=next)
306                     messages.add_message(request, status, message)
307                     return render_response(on_success,
308                            context_instance=get_context(request, extra_context))
309     except (Invitation.DoesNotExist, ValueError), e:
310         messages.add_message(request, messages.ERROR, e)
311         for provider in IM_MODULES:
312             main = provider.capitalize() if provider == 'local' else 'ThirdParty'
313             formclass = '%sUserCreationForm' % main
314             extra_context['%s_form' % provider] = globals()[formclass]()
315     return render_response(on_failure,
316                            context_instance=get_context(request, extra_context))
317
318 @login_required
319 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
320     """
321     Allows a user to send feedback.
322     
323     In case of GET request renders a form for providing the feedback information.
324     In case of POST sends an email to support team.
325     
326     If the user isn't logged in, redirects to settings.LOGIN_URL.
327     
328     **Arguments**
329     
330     ``template_name``
331         A custom template to use. This is optional; if not specified,
332         this will default to ``im/feedback.html``.
333     
334     ``extra_context``
335         An dictionary of variables to add to the template context.
336     
337     **Template:**
338     
339     im/signup.html or ``template_name`` keyword argument.
340     
341     **Settings:**
342     
343     * LOGIN_URL: login uri
344     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
345     """
346     if request.method == 'GET':
347         form = FeedbackForm()
348     if request.method == 'POST':
349         if not request.user:
350             return HttpResponse('Unauthorized', status=401)
351         
352         form = FeedbackForm(request.POST)
353         if form.is_valid():
354             subject = _("Feedback from %s" % SITENAME)
355             from_email = request.user.email
356             recipient_list = [DEFAULT_CONTACT_EMAIL]
357             content = render_to_string(email_template_name, {
358                         'message': form.cleaned_data['feedback_msg'],
359                         'data': form.cleaned_data['feedback_data'],
360                         'request': request})
361             
362             try:
363                 send_mail(subject, content, from_email, recipient_list)
364                 message = _('Feedback successfully sent')
365                 status = messages.SUCCESS
366             except (SMTPException, socket.error) as e:
367                 status = messages.ERROR
368                 message = getattr(e, 'strerror', '')
369             messages.add_message(request, status, message)
370     return render_response(template_name,
371                            form = form,
372                            context_instance = get_context(request, extra_context))
373
374 def logout(request, template='registration/logged_out.html', extra_context={}):
375     """
376     Wraps `django.contrib.auth.logout` and delete the cookie.
377     """
378     auth_logout(request)
379     response = HttpResponse()
380     response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
381     next = request.GET.get('next')
382     if next:
383         response['Location'] = next
384         response.status_code = 302
385         return response
386     context = get_context(request, extra_context)
387     response.write(render_to_string(template, context_instance=context))
388     return response
389
390 def activate(request):
391     """
392     Activates the user identified by the ``auth`` request parameter
393     """
394     token = request.GET.get('auth')
395     next = request.GET.get('next')
396     try:
397         user = AstakosUser.objects.get(auth_token=token)
398     except AstakosUser.DoesNotExist:
399         return HttpResponseBadRequest('No such user')
400     
401     user.is_active = True
402     user.save()
403     return prepare_response(request, user, next, renew=True)