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